Compare commits

..

No commits in common. "9d7306c395854440318c4683ef74f16de9f8d14c" and "5101c3665a4c51eebda4797ee5984f804959a4e6" have entirely different histories.

57 changed files with 342 additions and 4361 deletions

3
.gitignore vendored
View File

@ -6,9 +6,6 @@
# dotenv files # dotenv files
.env .env
appsettings.Local.json appsettings.Local.json
appsettings*.json
line-gestao-api.csproj
line-gestao-api.http
# User-specific files # User-specific files
*.rsuser *.rsuser

View File

@ -1,5 +1,4 @@
using System.Globalization; 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.Data;
@ -23,24 +22,21 @@ public class AuthController : ControllerBase
private readonly AppDbContext _db; private readonly AppDbContext _db;
private readonly ITenantProvider _tenantProvider; private readonly ITenantProvider _tenantProvider;
private readonly IConfiguration _config; private readonly IConfiguration _config;
private readonly ILogger<AuthController> _logger;
public AuthController( public AuthController(
UserManager<ApplicationUser> userManager, UserManager<ApplicationUser> userManager,
AppDbContext db, AppDbContext db,
ITenantProvider tenantProvider, ITenantProvider tenantProvider,
IConfiguration config, IConfiguration config)
ILogger<AuthController> logger)
{ {
_userManager = userManager; _userManager = userManager;
_db = db; _db = db;
_tenantProvider = tenantProvider; _tenantProvider = tenantProvider;
_config = config; _config = config;
_logger = logger;
} }
[HttpPost("register")] [HttpPost("register")]
[Authorize(Roles = "sysadmin")] [Authorize(Roles = "admin")]
public async Task<IActionResult> Register(RegisterRequest req) public async Task<IActionResult> Register(RegisterRequest req)
{ {
if (req.Password != req.ConfirmPassword) if (req.Password != req.ConfirmPassword)
@ -73,14 +69,12 @@ public class AuthController : ControllerBase
if (!createResult.Succeeded) if (!createResult.Succeeded)
return BadRequest(createResult.Errors.Select(e => e.Description).ToList()); return BadRequest(createResult.Errors.Select(e => e.Description).ToList());
await _userManager.AddToRoleAsync(user, AppRoles.Cliente); await _userManager.AddToRoleAsync(user, "leitura");
var effectiveTenantId = await EnsureValidTenantIdAsync(user); var effectiveTenantId = await EnsureValidTenantIdAsync(user);
if (!effectiveTenantId.HasValue) if (!effectiveTenantId.HasValue)
return Unauthorized("Tenant inválido."); return Unauthorized("Tenant inválido.");
await EnsureClientTenantDataBoundAsync(user, effectiveTenantId.Value);
var token = await GenerateJwtAsync(user, effectiveTenantId.Value); var token = await GenerateJwtAsync(user, effectiveTenantId.Value);
return Ok(new AuthResponse(token)); return Ok(new AuthResponse(token));
} }
@ -147,8 +141,6 @@ public class AuthController : ControllerBase
if (!effectiveTenantId.HasValue) if (!effectiveTenantId.HasValue)
return Unauthorized("Tenant inválido."); return Unauthorized("Tenant inválido.");
await EnsureClientTenantDataBoundAsync(user, effectiveTenantId.Value);
var token = await GenerateJwtAsync(user, effectiveTenantId.Value); var token = await GenerateJwtAsync(user, effectiveTenantId.Value);
return Ok(new AuthResponse(token)); return Ok(new AuthResponse(token));
} }
@ -200,216 +192,23 @@ public class AuthController : ControllerBase
private async Task<Guid?> EnsureValidTenantIdAsync(ApplicationUser user) private async Task<Guid?> EnsureValidTenantIdAsync(ApplicationUser user)
{ {
if (user.TenantId == Guid.Empty) if (user.TenantId != Guid.Empty)
{ return user.TenantId;
return null;
}
var existsAndActive = await _db.Tenants var fallbackTenantId = await _db.Tenants
.AsNoTracking() .AsNoTracking()
.AnyAsync(t => t.Id == user.TenantId && t.Ativo); .OrderBy(t => t.CreatedAt)
.Select(t => (Guid?)t.Id)
.FirstOrDefaultAsync();
if (!existsAndActive) if (!fallbackTenantId.HasValue || fallbackTenantId.Value == Guid.Empty)
{ return null;
user.TenantId = fallbackTenantId.Value;
var updateResult = await _userManager.UpdateAsync(user);
if (!updateResult.Succeeded)
return null; return null;
}
return user.TenantId; return user.TenantId;
} }
private async Task EnsureClientTenantDataBoundAsync(ApplicationUser user, Guid tenantId)
{
var roles = await _userManager.GetRolesAsync(user);
if (!roles.Any(r => string.Equals(r, AppRoles.Cliente, StringComparison.OrdinalIgnoreCase)))
{
return;
}
try
{
await RebindMobileLinesToTenantBySourceKeyAsync(tenantId);
}
catch (Exception ex)
{
_logger.LogError(ex, "Falha ao sincronizar linhas para tenant {TenantId} durante login de cliente.", tenantId);
}
}
private async Task<int> RebindMobileLinesToTenantBySourceKeyAsync(Guid tenantId)
{
if (tenantId == Guid.Empty)
{
return 0;
}
var tenant = await _db.Tenants
.AsNoTracking()
.FirstOrDefaultAsync(t => t.Id == tenantId && t.Ativo && !t.IsSystem);
if (tenant == null)
{
return 0;
}
if (!string.Equals(
tenant.SourceType,
SystemTenantConstants.MobileLinesClienteSourceType,
StringComparison.OrdinalIgnoreCase))
{
return 0;
}
var normalizedKeys = new HashSet<string>(StringComparer.Ordinal);
AddNormalizedTenantKey(normalizedKeys, tenant.SourceKey);
AddNormalizedTenantKey(normalizedKeys, tenant.NomeOficial);
if (normalizedKeys.Count == 0)
{
return 0;
}
var candidates = await _db.MobileLines
.IgnoreQueryFilters()
.Where(x => x.TenantId != tenant.Id)
.Where(x => x.Cliente != null && x.Cliente != string.Empty)
.ToListAsync();
if (candidates.Count == 0)
{
return 0;
}
var hasAnyTenantLine = await _db.MobileLines
.IgnoreQueryFilters()
.AsNoTracking()
.AnyAsync(x => x.TenantId == tenant.Id);
var now = DateTime.UtcNow;
var reassigned = ReassignByMatcher(
candidates,
normalizedKeys,
tenant.Id,
now,
isRelaxedMatch: false);
if (reassigned == 0 && !hasAnyTenantLine)
{
reassigned = ReassignByMatcher(
candidates,
normalizedKeys,
tenant.Id,
now,
isRelaxedMatch: true);
}
if (reassigned > 0)
{
await _db.SaveChangesAsync();
}
return reassigned;
}
private static int ReassignByMatcher(
IReadOnlyList<MobileLine> candidates,
IReadOnlyCollection<string> normalizedKeys,
Guid tenantId,
DateTime now,
bool isRelaxedMatch)
{
if (normalizedKeys.Count == 0)
{
return 0;
}
var keys = isRelaxedMatch
? normalizedKeys.Where(k => k.Length >= 6).ToList()
: normalizedKeys.ToList();
if (keys.Count == 0)
{
return 0;
}
var reassigned = 0;
foreach (var line in candidates)
{
var normalizedClient = NormalizeTenantKey(line.Cliente ?? string.Empty);
if (string.IsNullOrWhiteSpace(normalizedClient) ||
string.Equals(normalizedClient, "RESERVA", StringComparison.Ordinal))
{
continue;
}
var matches = !isRelaxedMatch
? keys.Contains(normalizedClient, StringComparer.Ordinal)
: keys.Any(k =>
normalizedClient.Contains(k, StringComparison.Ordinal) ||
k.Contains(normalizedClient, StringComparison.Ordinal));
if (!matches)
{
continue;
}
line.TenantId = tenantId;
line.UpdatedAt = now;
reassigned++;
}
return reassigned;
}
private static void AddNormalizedTenantKey(ISet<string> keys, string? rawKey)
{
var normalized = NormalizeTenantKey(rawKey ?? string.Empty);
if (string.IsNullOrWhiteSpace(normalized))
{
return;
}
if (string.Equals(normalized, "RESERVA", StringComparison.Ordinal))
{
return;
}
keys.Add(normalized);
}
private static string NormalizeTenantKey(string value)
{
if (string.IsNullOrWhiteSpace(value))
{
return string.Empty;
}
var normalized = value.Trim().Normalize(NormalizationForm.FormD);
var sb = new StringBuilder(normalized.Length);
var previousWasSpace = false;
foreach (var ch in normalized)
{
var category = CharUnicodeInfo.GetUnicodeCategory(ch);
if (category == UnicodeCategory.NonSpacingMark)
{
continue;
}
if (char.IsWhiteSpace(ch))
{
if (!previousWasSpace)
{
sb.Append(' ');
previousWasSpace = true;
}
continue;
}
sb.Append(char.ToUpperInvariant(ch));
previousWasSpace = false;
}
return sb.ToString().Trim();
}
} }

View File

@ -10,7 +10,7 @@ namespace line_gestao_api.Controllers
{ {
[ApiController] [ApiController]
[Route("api/[controller]")] [Route("api/[controller]")]
[Authorize(Roles = "sysadmin,gestor")] [Authorize(Roles = "admin")]
public class BillingController : ControllerBase public class BillingController : ControllerBase
{ {
private readonly AppDbContext _db; private readonly AppDbContext _db;
@ -43,20 +43,11 @@ namespace line_gestao_api.Controllers
var s = search.Trim(); var s = search.Trim();
var hasNumberSearch = TryParseSearchDecimal(s, out var searchNumber); var hasNumberSearch = TryParseSearchDecimal(s, out var searchNumber);
var hasIntSearch = int.TryParse(new string(s.Where(char.IsDigit).ToArray()), out var searchInt); var hasIntSearch = int.TryParse(new string(s.Where(char.IsDigit).ToArray()), out var searchInt);
var matchingClientsByLineOrChip = _db.MobileLines.AsNoTracking()
.Where(m =>
EF.Functions.ILike(m.Linha ?? "", $"%{s}%") ||
EF.Functions.ILike(m.Chip ?? "", $"%{s}%"))
.Where(m => m.Cliente != null && m.Cliente != "")
.Select(m => m.Cliente!)
.Distinct();
q = q.Where(x => EF.Functions.ILike(x.Cliente ?? "", $"%{s}%") q = q.Where(x => EF.Functions.ILike(x.Cliente ?? "", $"%{s}%")
|| EF.Functions.ILike(x.Tipo ?? "", $"%{s}%") || EF.Functions.ILike(x.Tipo ?? "", $"%{s}%")
|| EF.Functions.ILike(x.Item.ToString(), $"%{s}%") || EF.Functions.ILike(x.Item.ToString(), $"%{s}%")
|| EF.Functions.ILike(x.Aparelho ?? "", $"%{s}%") || EF.Functions.ILike(x.Aparelho ?? "", $"%{s}%")
|| EF.Functions.ILike(x.FormaPagamento ?? "", $"%{s}%") || EF.Functions.ILike(x.FormaPagamento ?? "", $"%{s}%")
|| (x.Cliente != null && matchingClientsByLineOrChip.Contains(x.Cliente))
|| (hasIntSearch && (x.QtdLinhas ?? 0) == searchInt) || (hasIntSearch && (x.QtdLinhas ?? 0) == searchInt)
|| (hasNumberSearch && || (hasNumberSearch &&
(((x.FranquiaVivo ?? 0m) == searchNumber) || (((x.FranquiaVivo ?? 0m) == searchNumber) ||
@ -197,7 +188,7 @@ namespace line_gestao_api.Controllers
} }
[HttpPut("{id:guid}")] [HttpPut("{id:guid}")]
[Authorize(Roles = "sysadmin,gestor")] [Authorize(Roles = "admin")]
public async Task<IActionResult> Update(Guid id, [FromBody] UpdateBillingClientRequest req) public async Task<IActionResult> Update(Guid id, [FromBody] UpdateBillingClientRequest req)
{ {
var x = await _db.BillingClients.FirstOrDefaultAsync(a => a.Id == id); var x = await _db.BillingClients.FirstOrDefaultAsync(a => a.Id == id);
@ -230,7 +221,7 @@ namespace line_gestao_api.Controllers
} }
[HttpDelete("{id:guid}")] [HttpDelete("{id:guid}")]
[Authorize(Roles = "sysadmin,gestor")] [Authorize(Roles = "admin")]
public async Task<IActionResult> Delete(Guid id) public async Task<IActionResult> Delete(Guid id)
{ {
var x = await _db.BillingClients.FirstOrDefaultAsync(a => a.Id == id); var x = await _db.BillingClients.FirstOrDefaultAsync(a => a.Id == id);

View File

@ -11,7 +11,7 @@ namespace line_gestao_api.Controllers
{ {
[ApiController] [ApiController]
[Route("api/chips-virgens")] [Route("api/chips-virgens")]
[Authorize(Roles = "sysadmin,gestor")] [Authorize(Roles = "admin")]
public class ChipsVirgensController : ControllerBase public class ChipsVirgensController : ControllerBase
{ {
private readonly AppDbContext _db; private readonly AppDbContext _db;
@ -93,7 +93,7 @@ namespace line_gestao_api.Controllers
} }
[HttpPost] [HttpPost]
[Authorize(Roles = "sysadmin,gestor")] [Authorize(Roles = "admin,gestor")]
public async Task<ActionResult<ChipVirgemDetailDto>> Create([FromBody] CreateChipVirgemDto req) public async Task<ActionResult<ChipVirgemDetailDto>> Create([FromBody] CreateChipVirgemDto req)
{ {
var now = DateTime.UtcNow; var now = DateTime.UtcNow;
@ -122,7 +122,7 @@ namespace line_gestao_api.Controllers
} }
[HttpPut("{id:guid}")] [HttpPut("{id:guid}")]
[Authorize(Roles = "sysadmin,gestor")] [Authorize(Roles = "admin")]
public async Task<IActionResult> Update(Guid id, [FromBody] UpdateChipVirgemRequest req) public async Task<IActionResult> Update(Guid id, [FromBody] UpdateChipVirgemRequest req)
{ {
var x = await _db.ChipVirgemLines.FirstOrDefaultAsync(a => a.Id == id); var x = await _db.ChipVirgemLines.FirstOrDefaultAsync(a => a.Id == id);
@ -139,7 +139,7 @@ namespace line_gestao_api.Controllers
} }
[HttpDelete("{id:guid}")] [HttpDelete("{id:guid}")]
[Authorize(Roles = "sysadmin,gestor")] [Authorize(Roles = "admin")]
public async Task<IActionResult> Delete(Guid id) public async Task<IActionResult> Delete(Guid id)
{ {
var x = await _db.ChipVirgemLines.FirstOrDefaultAsync(a => a.Id == id); var x = await _db.ChipVirgemLines.FirstOrDefaultAsync(a => a.Id == id);

View File

@ -11,7 +11,7 @@ namespace line_gestao_api.Controllers
{ {
[ApiController] [ApiController]
[Route("api/controle-recebidos")] [Route("api/controle-recebidos")]
[Authorize(Roles = "sysadmin,gestor")] [Authorize(Roles = "admin")]
public class ControleRecebidosController : ControllerBase public class ControleRecebidosController : ControllerBase
{ {
private readonly AppDbContext _db; private readonly AppDbContext _db;
@ -138,7 +138,7 @@ namespace line_gestao_api.Controllers
} }
[HttpPost] [HttpPost]
[Authorize(Roles = "sysadmin,gestor")] [Authorize(Roles = "admin,gestor")]
public async Task<ActionResult<ControleRecebidoDetailDto>> Create([FromBody] CreateControleRecebidoDto req) public async Task<ActionResult<ControleRecebidoDetailDto>> Create([FromBody] CreateControleRecebidoDto req)
{ {
var now = DateTime.UtcNow; var now = DateTime.UtcNow;
@ -195,7 +195,7 @@ namespace line_gestao_api.Controllers
} }
[HttpPut("{id:guid}")] [HttpPut("{id:guid}")]
[Authorize(Roles = "sysadmin,gestor")] [Authorize(Roles = "admin")]
public async Task<IActionResult> Update(Guid id, [FromBody] UpdateControleRecebidoRequest req) public async Task<IActionResult> Update(Guid id, [FromBody] UpdateControleRecebidoRequest req)
{ {
var x = await _db.ControleRecebidoLines.FirstOrDefaultAsync(a => a.Id == id); var x = await _db.ControleRecebidoLines.FirstOrDefaultAsync(a => a.Id == id);
@ -223,7 +223,7 @@ namespace line_gestao_api.Controllers
} }
[HttpDelete("{id:guid}")] [HttpDelete("{id:guid}")]
[Authorize(Roles = "sysadmin,gestor")] [Authorize(Roles = "admin")]
public async Task<IActionResult> Delete(Guid id) public async Task<IActionResult> Delete(Guid id)
{ {
var x = await _db.ControleRecebidoLines.FirstOrDefaultAsync(a => a.Id == id); var x = await _db.ControleRecebidoLines.FirstOrDefaultAsync(a => a.Id == id);

View File

@ -11,7 +11,7 @@ namespace line_gestao_api.Controllers;
[ApiController] [ApiController]
[Route("api/historico")] [Route("api/historico")]
[Authorize(Roles = "sysadmin,gestor")] [Authorize(Roles = "admin")]
public class HistoricoController : ControllerBase public class HistoricoController : ControllerBase
{ {
private readonly AppDbContext _db; private readonly AppDbContext _db;
@ -26,7 +26,7 @@ public class HistoricoController : ControllerBase
[FromQuery] string? pageName, [FromQuery] string? pageName,
[FromQuery] string? action, [FromQuery] string? action,
[FromQuery] string? entity, [FromQuery] string? entity,
[FromQuery] string? user, [FromQuery] Guid? userId,
[FromQuery] string? search, [FromQuery] string? search,
[FromQuery] DateTime? dateFrom, [FromQuery] DateTime? dateFrom,
[FromQuery] DateTime? dateTo, [FromQuery] DateTime? dateTo,
@ -60,17 +60,15 @@ public class HistoricoController : ControllerBase
q = q.Where(x => EF.Functions.ILike(x.EntityName, $"%{e}%")); q = q.Where(x => EF.Functions.ILike(x.EntityName, $"%{e}%"));
} }
if (!string.IsNullOrWhiteSpace(user)) if (userId.HasValue)
{ {
var u = user.Trim(); q = q.Where(x => x.UserId == userId.Value);
q = q.Where(x =>
EF.Functions.ILike(x.UserName ?? "", $"%{u}%") ||
EF.Functions.ILike(x.UserEmail ?? "", $"%{u}%"));
} }
if (!string.IsNullOrWhiteSpace(search)) if (!string.IsNullOrWhiteSpace(search))
{ {
var s = search.Trim(); var s = search.Trim();
var hasGuidSearch = Guid.TryParse(s, out var searchGuid);
var hasDateSearch = TryParseSearchDateUtcStart(s, out var searchDateStartUtc); var hasDateSearch = TryParseSearchDateUtcStart(s, out var searchDateStartUtc);
var searchDateEndUtc = hasDateSearch ? searchDateStartUtc.AddDays(1) : DateTime.MinValue; var searchDateEndUtc = hasDateSearch ? searchDateStartUtc.AddDays(1) : DateTime.MinValue;
q = q.Where(x => q = q.Where(x =>
@ -85,6 +83,7 @@ public class HistoricoController : ControllerBase
EF.Functions.ILike(x.RequestMethod ?? "", $"%{s}%") || EF.Functions.ILike(x.RequestMethod ?? "", $"%{s}%") ||
EF.Functions.ILike(x.IpAddress ?? "", $"%{s}%") || EF.Functions.ILike(x.IpAddress ?? "", $"%{s}%") ||
// ChangesJson is stored as jsonb; applying ILIKE directly causes PostgreSQL 42883. // ChangesJson is stored as jsonb; applying ILIKE directly causes PostgreSQL 42883.
(hasGuidSearch && (x.Id == searchGuid || x.UserId == searchGuid)) ||
(hasDateSearch && (hasDateSearch &&
x.OccurredAtUtc >= searchDateStartUtc && x.OccurredAtUtc >= searchDateStartUtc &&
x.OccurredAtUtc < searchDateEndUtc)); x.OccurredAtUtc < searchDateEndUtc));

File diff suppressed because it is too large Load Diff

View File

@ -187,7 +187,7 @@ namespace line_gestao_api.Controllers
} }
[HttpPost] [HttpPost]
[Authorize(Roles = "sysadmin,gestor")] [Authorize(Roles = "admin,gestor")]
public async Task<ActionResult<MuregDetailDto>> Create([FromBody] CreateMuregDto req) public async Task<ActionResult<MuregDetailDto>> Create([FromBody] CreateMuregDto req)
{ {
if (req.MobileLineId == Guid.Empty) if (req.MobileLineId == Guid.Empty)
@ -289,7 +289,7 @@ namespace line_gestao_api.Controllers
} }
[HttpPut("{id:guid}")] [HttpPut("{id:guid}")]
[Authorize(Roles = "sysadmin,gestor")] [Authorize(Roles = "admin,gestor")]
public async Task<IActionResult> Update(Guid id, [FromBody] UpdateMuregDto req) public async Task<IActionResult> Update(Guid id, [FromBody] UpdateMuregDto req)
{ {
var entity = await _db.MuregLines.FirstOrDefaultAsync(x => x.Id == id); var entity = await _db.MuregLines.FirstOrDefaultAsync(x => x.Id == id);
@ -361,7 +361,7 @@ namespace line_gestao_api.Controllers
// Exclui registro MUREG // Exclui registro MUREG
// ========================================================== // ==========================================================
[HttpDelete("{id:guid}")] [HttpDelete("{id:guid}")]
[Authorize(Roles = "sysadmin")] [Authorize(Roles = "admin")]
public async Task<IActionResult> Delete(Guid id) public async Task<IActionResult> Delete(Guid id)
{ {
var entity = await _db.MuregLines.FirstOrDefaultAsync(x => x.Id == id); var entity = await _db.MuregLines.FirstOrDefaultAsync(x => x.Id == id);
@ -377,7 +377,7 @@ namespace line_gestao_api.Controllers
// ✅ POST: /api/mureg/import-excel (mantido) // ✅ POST: /api/mureg/import-excel (mantido)
// ========================================================== // ==========================================================
[HttpPost("import-excel")] [HttpPost("import-excel")]
[Authorize(Roles = "sysadmin,gestor")] [Authorize(Roles = "admin,gestor")]
[Consumes("multipart/form-data")] [Consumes("multipart/form-data")]
[RequestSizeLimit(50_000_000)] [RequestSizeLimit(50_000_000)]
public async Task<IActionResult> ImportExcel([FromForm] ImportExcelForm form) public async Task<IActionResult> ImportExcel([FromForm] ImportExcelForm form)

View File

@ -45,9 +45,7 @@ public class NotificationsController : ControllerBase
DiasParaVencer = notification.DiasParaVencer, DiasParaVencer = notification.DiasParaVencer,
Lida = notification.Lida, Lida = notification.Lida,
LidaEm = notification.LidaEm, LidaEm = notification.LidaEm,
VigenciaLineId = notification.VigenciaLineId VigenciaLineId = notification.VigenciaLineId,
?? (vigencia != null ? (Guid?)vigencia.Id : null)
?? (vigenciaByLinha != null ? (Guid?)vigenciaByLinha.Id : null),
Cliente = notification.Cliente Cliente = notification.Cliente
?? (vigencia != null ? vigencia.Cliente : null) ?? (vigencia != null ? vigencia.Cliente : null)
?? (vigenciaByLinha != null ? vigenciaByLinha.Cliente : null), ?? (vigenciaByLinha != null ? vigenciaByLinha.Cliente : null),

View File

@ -9,7 +9,7 @@ namespace line_gestao_api.Controllers;
[ApiController] [ApiController]
[Route("api/parcelamentos")] [Route("api/parcelamentos")]
[Authorize(Roles = "sysadmin,gestor")] [Authorize(Roles = "admin")]
public class ParcelamentosController : ControllerBase public class ParcelamentosController : ControllerBase
{ {
private readonly AppDbContext _db; private readonly AppDbContext _db;
@ -165,7 +165,7 @@ public class ParcelamentosController : ControllerBase
} }
[HttpPost] [HttpPost]
[Authorize(Roles = "sysadmin,gestor")] [Authorize(Roles = "admin,gestor")]
public async Task<ActionResult<ParcelamentoDetailDto>> Create([FromBody] ParcelamentoUpsertDto req) public async Task<ActionResult<ParcelamentoDetailDto>> Create([FromBody] ParcelamentoUpsertDto req)
{ {
var now = DateTime.UtcNow; var now = DateTime.UtcNow;
@ -202,7 +202,7 @@ public class ParcelamentosController : ControllerBase
} }
[HttpPut("{id:guid}")] [HttpPut("{id:guid}")]
[Authorize(Roles = "sysadmin,gestor")] [Authorize(Roles = "admin,gestor")]
public async Task<IActionResult> Update(Guid id, [FromBody] ParcelamentoUpsertDto req) public async Task<IActionResult> Update(Guid id, [FromBody] ParcelamentoUpsertDto req)
{ {
var entity = await _db.ParcelamentoLines var entity = await _db.ParcelamentoLines
@ -239,7 +239,7 @@ public class ParcelamentosController : ControllerBase
} }
[HttpDelete("{id:guid}")] [HttpDelete("{id:guid}")]
[Authorize(Roles = "sysadmin,gestor")] [Authorize(Roles = "admin")]
public async Task<IActionResult> Delete(Guid id) public async Task<IActionResult> Delete(Guid id)
{ {
var entity = await _db.ParcelamentoLines.FirstOrDefaultAsync(x => x.Id == id); var entity = await _db.ParcelamentoLines.FirstOrDefaultAsync(x => x.Id == id);

View File

@ -32,7 +32,6 @@ namespace line_gestao_api.Controllers
// GERAL (MobileLines) // GERAL (MobileLines)
// ========================= // =========================
var qLines = _db.MobileLines.AsNoTracking(); var qLines = _db.MobileLines.AsNoTracking();
var qLinesWithClient = qLines.Where(x => x.Cliente != null && x.Cliente != "");
var totalLinhas = await qLines.CountAsync(); var totalLinhas = await qLines.CountAsync();
@ -45,35 +44,27 @@ namespace line_gestao_api.Controllers
var ativos = await qLines.CountAsync(x => var ativos = await qLines.CountAsync(x =>
EF.Functions.ILike((x.Status ?? "").Trim(), "%ativo%")); EF.Functions.ILike((x.Status ?? "").Trim(), "%ativo%"));
var bloqueadosPerdaRoubo = await qLinesWithClient.CountAsync(x => var bloqueadosPerdaRoubo = await qLines.CountAsync(x =>
EF.Functions.ILike((x.Status ?? "").Trim(), "%perda%") || EF.Functions.ILike((x.Status ?? "").Trim(), "%perda%") ||
EF.Functions.ILike((x.Status ?? "").Trim(), "%roubo%")); EF.Functions.ILike((x.Status ?? "").Trim(), "%roubo%"));
var bloqueados120Dias = await qLinesWithClient.CountAsync(x => var bloqueados120Dias = await qLines.CountAsync(x =>
EF.Functions.ILike((x.Status ?? "").Trim(), "%bloque%") && EF.Functions.ILike((x.Status ?? "").Trim(), "%bloque%") &&
EF.Functions.ILike((x.Status ?? "").Trim(), "%120%") && EF.Functions.ILike((x.Status ?? "").Trim(), "%120%") &&
EF.Functions.ILike((x.Status ?? "").Trim(), "%dia%") && EF.Functions.ILike((x.Status ?? "").Trim(), "%dia%"));
!(EF.Functions.ILike((x.Status ?? "").Trim(), "%perda%") ||
EF.Functions.ILike((x.Status ?? "").Trim(), "%roubo%")));
var bloqueadosOutros = await qLinesWithClient.CountAsync(x => var bloqueadosOutros = await qLines.CountAsync(x =>
EF.Functions.ILike((x.Status ?? "").Trim(), "%bloque%") && EF.Functions.ILike((x.Status ?? "").Trim(), "%bloque%") &&
!(EF.Functions.ILike((x.Status ?? "").Trim(), "%120%") && EF.Functions.ILike((x.Status ?? "").Trim(), "%dia%")) && !(EF.Functions.ILike((x.Status ?? "").Trim(), "%120%") && EF.Functions.ILike((x.Status ?? "").Trim(), "%dia%")) &&
!(EF.Functions.ILike((x.Status ?? "").Trim(), "%perda%") || EF.Functions.ILike((x.Status ?? "").Trim(), "%roubo%")) !(EF.Functions.ILike((x.Status ?? "").Trim(), "%perda%") || EF.Functions.ILike((x.Status ?? "").Trim(), "%roubo%"))
); );
// Regra do KPI "Bloqueadas" alinhada ao critério da página Geral: var bloqueados = bloqueadosPerdaRoubo + bloqueados120Dias + bloqueadosOutros;
// status contendo "bloque", "perda" ou "roubo".
var bloqueados = await qLinesWithClient.CountAsync(x =>
EF.Functions.ILike((x.Status ?? "").Trim(), "%bloque%") ||
EF.Functions.ILike((x.Status ?? "").Trim(), "%perda%") ||
EF.Functions.ILike((x.Status ?? "").Trim(), "%roubo%"));
// Regra alinhada ao filtro "Reservas" da página Geral.
var reservas = await qLines.CountAsync(x => var reservas = await qLines.CountAsync(x =>
EF.Functions.ILike((x.Usuario ?? "").Trim(), "RESERVA") || EF.Functions.ILike((x.Cliente ?? "").Trim(), "%RESERVA%") ||
EF.Functions.ILike((x.Skil ?? "").Trim(), "RESERVA") || EF.Functions.ILike((x.Usuario ?? "").Trim(), "%RESERVA%") ||
EF.Functions.ILike((x.Cliente ?? "").Trim(), "RESERVA")); EF.Functions.ILike((x.Skil ?? "").Trim(), "%RESERVA%"));
var topClientes = await qLines var topClientes = await qLines
.Where(x => x.Cliente != null && x.Cliente != "") .Where(x => x.Cliente != null && x.Cliente != "")

View File

@ -1,5 +1,3 @@
using System.Text.RegularExpressions;
using System.Linq;
using line_gestao_api.Data; using line_gestao_api.Data;
using line_gestao_api.Dtos; using line_gestao_api.Dtos;
using line_gestao_api.Services; using line_gestao_api.Services;
@ -14,8 +12,6 @@ namespace line_gestao_api.Controllers;
[Authorize] [Authorize]
public class ResumoController : ControllerBase public class ResumoController : ControllerBase
{ {
private static readonly Regex PlanGbRegex = new(@"(\d+(?:[.,]\d+)?)\s*(GB|MB)\b", RegexOptions.IgnoreCase | RegexOptions.Compiled);
private readonly AppDbContext _db; private readonly AppDbContext _db;
private readonly SpreadsheetImportAuditService _spreadsheetImportAuditService; private readonly SpreadsheetImportAuditService _spreadsheetImportAuditService;
@ -27,36 +23,6 @@ public class ResumoController : ControllerBase
[HttpGet] [HttpGet]
public async Task<ActionResult<ResumoResponseDto>> GetResumo() public async Task<ActionResult<ResumoResponseDto>> GetResumo()
{
var spreadsheetResponse = await BuildSpreadsheetResumoAsync();
var hasLiveLines = await _db.MobileLines.AsNoTracking().AnyAsync();
if (hasLiveLines)
{
var live = await BuildLiveResumoAsync();
spreadsheetResponse.MacrophonyPlans = live.MacrophonyPlans;
spreadsheetResponse.MacrophonyTotals = live.MacrophonyTotals;
spreadsheetResponse.VivoLineResumos = live.VivoLineResumos;
spreadsheetResponse.VivoLineTotals = live.VivoLineTotals;
spreadsheetResponse.PlanoContratoResumos = live.PlanoContratoResumos;
spreadsheetResponse.PlanoContratoTotal = live.PlanoContratoTotal;
spreadsheetResponse.LineTotais = live.LineTotais;
spreadsheetResponse.GbDistribuicao = live.GbDistribuicao;
spreadsheetResponse.GbDistribuicaoTotal = live.GbDistribuicaoTotal;
spreadsheetResponse.ReservaLines = live.ReservaLines;
spreadsheetResponse.ReservaPorDdd = live.ReservaPorDdd;
spreadsheetResponse.TotalGeralLinhasReserva = live.TotalGeralLinhasReserva;
spreadsheetResponse.ReservaTotal = live.ReservaTotal;
}
spreadsheetResponse.MacrophonyTotals ??= new ResumoMacrophonyTotalDto();
spreadsheetResponse.VivoLineTotals ??= new ResumoVivoLineTotalDto();
return Ok(spreadsheetResponse);
}
private async Task<ResumoResponseDto> BuildSpreadsheetResumoAsync()
{ {
var reservaLines = await _db.ResumoReservaLines.AsNoTracking() var reservaLines = await _db.ResumoReservaLines.AsNoTracking()
.OrderBy(x => x.Ddd) .OrderBy(x => x.Ddd)
@ -211,302 +177,6 @@ public class ResumoController : ControllerBase
response.VivoLineTotals ??= new ResumoVivoLineTotalDto(); response.VivoLineTotals ??= new ResumoVivoLineTotalDto();
response.VivoLineTotals.QtdLinhasTotal = canonicalTotalLinhas; response.VivoLineTotals.QtdLinhasTotal = canonicalTotalLinhas;
return response; return Ok(response);
}
private async Task<ResumoResponseDto> BuildLiveResumoAsync()
{
var allLines = _db.MobileLines.AsNoTracking();
var nonReservaLines = allLines.Where(x =>
!EF.Functions.ILike((x.Usuario ?? "").Trim(), "RESERVA") &&
!EF.Functions.ILike((x.Skil ?? "").Trim(), "RESERVA") &&
!EF.Functions.ILike((x.Cliente ?? "").Trim(), "RESERVA"));
var reservaLinesQuery = allLines.Where(x =>
EF.Functions.ILike((x.Usuario ?? "").Trim(), "RESERVA") ||
EF.Functions.ILike((x.Skil ?? "").Trim(), "RESERVA") ||
EF.Functions.ILike((x.Cliente ?? "").Trim(), "RESERVA"));
var planosAgg = await nonReservaLines
.GroupBy(x => (x.PlanoContrato ?? "").Trim())
.Select(g => new
{
Plano = g.Key,
TotalLinhas = g.Count(),
FranquiaMedia = g.Average(x => (decimal?)x.FranquiaVivo),
ValorTotal = g.Sum(x => x.ValorContratoVivo ?? 0m),
TravelCount = g.Count(x => (x.VivoTravelMundo ?? 0m) > 0m)
})
.ToListAsync();
var planoRows = planosAgg
.Select(row =>
{
var planoLabel = string.IsNullOrWhiteSpace(row.Plano) ? "SEM PLANO" : row.Plano;
var franquiaGb = (row.FranquiaMedia ?? 0m) > 0m
? row.FranquiaMedia
: ExtractGbFromPlanName(planoLabel);
var valorIndividual = row.TotalLinhas > 0 ? row.ValorTotal / row.TotalLinhas : (decimal?)null;
return new ResumoPlanoContratoResumoDto
{
PlanoContrato = planoLabel,
Gb = franquiaGb,
FranquiaGb = franquiaGb,
TotalLinhas = row.TotalLinhas,
ValorTotal = row.ValorTotal,
ValorIndividualComSvas = valorIndividual
};
})
.OrderByDescending(x => x.TotalLinhas ?? 0)
.ThenBy(x => x.PlanoContrato)
.ToList();
var macrophonyRows = planosAgg
.Select(row =>
{
var planoLabel = string.IsNullOrWhiteSpace(row.Plano) ? "SEM PLANO" : row.Plano;
var franquiaGb = (row.FranquiaMedia ?? 0m) > 0m
? row.FranquiaMedia
: ExtractGbFromPlanName(planoLabel);
var valorIndividual = row.TotalLinhas > 0 ? row.ValorTotal / row.TotalLinhas : (decimal?)null;
return new ResumoMacrophonyPlanDto
{
PlanoContrato = planoLabel,
Gb = franquiaGb,
FranquiaGb = franquiaGb,
TotalLinhas = row.TotalLinhas,
ValorTotal = row.ValorTotal,
ValorIndividualComSvas = valorIndividual,
VivoTravel = row.TravelCount > 0
};
})
.OrderByDescending(x => x.TotalLinhas ?? 0)
.ThenBy(x => x.PlanoContrato)
.ToList();
var clientesAgg = await nonReservaLines
.GroupBy(x => (x.Cliente ?? "").Trim())
.Select(g => new
{
Cliente = g.Key,
QtdLinhas = g.Count(),
FranquiaTotal = g.Sum(x => x.FranquiaVivo ?? 0m),
ValorContratoVivo = g.Sum(x => x.ValorContratoVivo ?? 0m),
FranquiaLine = g.Sum(x => x.FranquiaLine ?? 0m),
ValorContratoLine = g.Sum(x => x.ValorContratoLine ?? 0m),
Lucro = g.Sum(x => x.Lucro ?? 0m)
})
.ToListAsync();
var clientesRows = clientesAgg
.Select(row => new ResumoVivoLineResumoDto
{
Cliente = string.IsNullOrWhiteSpace(row.Cliente) ? "SEM CLIENTE" : row.Cliente,
QtdLinhas = row.QtdLinhas,
FranquiaTotal = row.FranquiaTotal,
ValorContratoVivo = row.ValorContratoVivo,
FranquiaLine = row.FranquiaLine,
ValorContratoLine = row.ValorContratoLine,
Lucro = row.Lucro
})
.OrderByDescending(x => x.QtdLinhas ?? 0)
.ThenBy(x => x.Cliente)
.ToList();
var totalLinhasNaoReserva = clientesRows.Sum(x => x.QtdLinhas ?? 0);
var totalFranquiaNaoReserva = clientesRows.Sum(x => x.FranquiaTotal ?? 0m);
var totalValorContratoVivo = clientesRows.Sum(x => x.ValorContratoVivo ?? 0m);
var totalFranquiaLine = clientesRows.Sum(x => x.FranquiaLine ?? 0m);
var totalValorContratoLine = clientesRows.Sum(x => x.ValorContratoLine ?? 0m);
var totalLucro = clientesRows.Sum(x => x.Lucro ?? 0m);
var pfAtivas = await nonReservaLines
.Where(x => EF.Functions.ILike((x.Status ?? "").Trim(), "%ativo%"))
.Where(x =>
EF.Functions.ILike((x.Skil ?? "").Trim(), "%fís%") ||
EF.Functions.ILike((x.Skil ?? "").Trim(), "%fis%") ||
EF.Functions.ILike((x.Skil ?? "").Trim(), "%pf%"))
.GroupBy(_ => 1)
.Select(g => new
{
QtdLinhas = g.Count(),
ValorTotal = g.Sum(x => x.ValorContratoLine ?? 0m),
LucroTotal = g.Sum(x => x.Lucro ?? 0m)
})
.FirstOrDefaultAsync();
var pjAtivas = await nonReservaLines
.Where(x => EF.Functions.ILike((x.Status ?? "").Trim(), "%ativo%"))
.Where(x =>
EF.Functions.ILike((x.Skil ?? "").Trim(), "%jur%") ||
EF.Functions.ILike((x.Skil ?? "").Trim(), "%pj%"))
.GroupBy(_ => 1)
.Select(g => new
{
QtdLinhas = g.Count(),
ValorTotal = g.Sum(x => x.ValorContratoLine ?? 0m),
LucroTotal = g.Sum(x => x.Lucro ?? 0m)
})
.FirstOrDefaultAsync();
var totaisLine = new List<ResumoLineTotaisDto>
{
new()
{
Tipo = "PF",
QtdLinhas = pfAtivas?.QtdLinhas ?? 0,
ValorTotalLine = pfAtivas?.ValorTotal ?? 0m,
LucroTotalLine = pfAtivas?.LucroTotal ?? 0m
},
new()
{
Tipo = "PJ",
QtdLinhas = pjAtivas?.QtdLinhas ?? 0,
ValorTotalLine = pjAtivas?.ValorTotal ?? 0m,
LucroTotalLine = pjAtivas?.LucroTotal ?? 0m
},
new()
{
Tipo = "TOTAL",
QtdLinhas = totalLinhasNaoReserva,
ValorTotalLine = totalValorContratoLine,
LucroTotalLine = totalLucro
}
};
var gbDistribuicao = await nonReservaLines
.Where(x => (x.FranquiaVivo ?? 0m) > 0m)
.GroupBy(x => x.FranquiaVivo ?? 0m)
.Select(g => new ResumoGbDistribuicaoDto
{
Gb = g.Key,
Qtd = g.Count(),
Soma = g.Sum(x => x.FranquiaVivo ?? 0m)
})
.OrderBy(x => x.Gb)
.ToListAsync();
var gbDistribuicaoTotal = new ResumoGbDistribuicaoTotalDto
{
TotalLinhas = gbDistribuicao.Sum(x => x.Qtd ?? 0),
SomaTotal = gbDistribuicao.Sum(x => x.Soma ?? 0m)
};
var reservaSnapshot = await reservaLinesQuery
.Select(x => new
{
x.Linha,
x.FranquiaVivo,
x.ValorContratoVivo
})
.ToListAsync();
var reservaGroupRaw = reservaSnapshot
.Select(row => new
{
Ddd = ExtractDddFromLine(row.Linha) ?? "-",
FranquiaGb = row.FranquiaVivo
})
.GroupBy(x => new { x.Ddd, x.FranquiaGb })
.Select(g => new ResumoReservaLineDto
{
Ddd = g.Key.Ddd,
FranquiaGb = g.Key.FranquiaGb,
QtdLinhas = g.Count(),
Total = null
})
.OrderBy(x => x.Ddd)
.ThenBy(x => x.FranquiaGb ?? 0m)
.ToList();
var reservaPorDdd = reservaGroupRaw
.GroupBy(x => x.Ddd ?? "-")
.Select(g => new ResumoReservaPorDddDto
{
Ddd = g.Key,
TotalLinhas = g.Sum(x => x.QtdLinhas ?? 0),
PorFranquia = g
.GroupBy(x => x.FranquiaGb)
.Select(fg => new ResumoReservaPorFranquiaDto
{
FranquiaGb = fg.Key,
TotalLinhas = fg.Sum(x => x.QtdLinhas ?? 0)
})
.OrderBy(x => x.FranquiaGb ?? 0m)
.ToList()
})
.OrderBy(x => x.Ddd)
.ToList();
var reservaTotalLinhas = reservaSnapshot.Count;
var reservaTotalValor = reservaSnapshot.Sum(x => x.ValorContratoVivo ?? 0m);
return new ResumoResponseDto
{
MacrophonyPlans = macrophonyRows,
MacrophonyTotals = new ResumoMacrophonyTotalDto
{
TotalLinhasTotal = totalLinhasNaoReserva,
FranquiaGbTotal = totalFranquiaNaoReserva,
ValorTotal = totalValorContratoVivo
},
VivoLineResumos = clientesRows,
VivoLineTotals = new ResumoVivoLineTotalDto
{
QtdLinhasTotal = totalLinhasNaoReserva,
FranquiaTotal = totalFranquiaNaoReserva,
ValorContratoVivo = totalValorContratoVivo,
FranquiaLine = totalFranquiaLine,
ValorContratoLine = totalValorContratoLine,
Lucro = totalLucro
},
PlanoContratoResumos = planoRows,
PlanoContratoTotal = new ResumoPlanoContratoTotalDto
{
ValorTotal = planoRows.Sum(x => x.ValorTotal ?? 0m)
},
LineTotais = totaisLine,
GbDistribuicao = gbDistribuicao,
GbDistribuicaoTotal = gbDistribuicaoTotal,
ReservaLines = reservaGroupRaw,
ReservaPorDdd = reservaPorDdd,
TotalGeralLinhasReserva = reservaTotalLinhas,
ReservaTotal = new ResumoReservaTotalDto
{
QtdLinhasTotal = reservaTotalLinhas,
Total = reservaTotalValor
}
};
}
private static decimal? ExtractGbFromPlanName(string? planoContrato)
{
if (string.IsNullOrWhiteSpace(planoContrato))
return null;
var match = PlanGbRegex.Match(planoContrato);
if (!match.Success)
return null;
var normalized = match.Groups[1].Value.Replace(',', '.');
if (!decimal.TryParse(normalized, out var rawValue))
return null;
var unit = match.Groups[2].Value.ToUpperInvariant();
return unit == "MB" ? rawValue / 1000m : rawValue;
}
private static string? ExtractDddFromLine(string? linha)
{
if (string.IsNullOrWhiteSpace(linha))
return null;
var digits = new string(linha.Where(char.IsDigit).ToArray());
if (digits.Length >= 12 && digits.StartsWith("55"))
return digits.Substring(2, 2);
if (digits.Length >= 10)
return digits.Substring(0, 2);
return null;
} }
} }

View File

@ -1,424 +0,0 @@
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;
using System.Globalization;
using System.Text;
namespace line_gestao_api.Controllers;
[ApiController]
[Route("api/system/tenants/{tenantId:guid}/users")]
[Authorize(Policy = "SystemAdmin")]
public class SystemTenantUsersController : ControllerBase
{
private readonly AppDbContext _db;
private readonly UserManager<ApplicationUser> _userManager;
private readonly RoleManager<IdentityRole<Guid>> _roleManager;
private readonly ISystemAuditService _systemAuditService;
public SystemTenantUsersController(
AppDbContext db,
UserManager<ApplicationUser> userManager,
RoleManager<IdentityRole<Guid>> roleManager,
ISystemAuditService systemAuditService)
{
_db = db;
_userManager = userManager;
_roleManager = roleManager;
_systemAuditService = systemAuditService;
}
[HttpPost]
public async Task<ActionResult<SystemTenantUserCreatedDto>> CreateUserForTenant(
[FromRoute] Guid tenantId,
[FromBody] CreateSystemTenantUserRequest request)
{
if (tenantId == Guid.Empty)
{
return await RejectAsync(tenantId, StatusCodes.Status400BadRequest, "TenantId inválido.", "invalid_tenant_id");
}
var tenant = await _db.Tenants.AsNoTracking().FirstOrDefaultAsync(t => t.Id == tenantId);
if (tenant == null)
{
return await RejectAsync(tenantId, StatusCodes.Status404NotFound, "Tenant não encontrado.", "tenant_not_found");
}
if (string.IsNullOrWhiteSpace(request.Email))
{
return await RejectAsync(tenantId, StatusCodes.Status400BadRequest, "Email é obrigatório.", "missing_email");
}
if (string.IsNullOrWhiteSpace(request.Password))
{
return await RejectAsync(tenantId, StatusCodes.Status400BadRequest, "Password é obrigatória.", "missing_password");
}
if (request.Roles == null || request.Roles.Count == 0)
{
return await RejectAsync(tenantId, StatusCodes.Status400BadRequest, "Informe ao menos uma role.", "missing_roles");
}
var normalizedRoles = request.Roles
.Where(r => !string.IsNullOrWhiteSpace(r))
.Select(r => r.Trim().ToLowerInvariant())
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();
if (normalizedRoles.Count == 0)
{
return await RejectAsync(tenantId, StatusCodes.Status400BadRequest, "Roles inválidas.", "invalid_roles");
}
var unsupportedRoles = normalizedRoles
.Where(role => !AppRoles.All.Contains(role, StringComparer.OrdinalIgnoreCase))
.ToList();
if (unsupportedRoles.Count > 0)
{
return await RejectAsync(
tenantId,
StatusCodes.Status400BadRequest,
$"Roles não suportadas: {string.Join(", ", unsupportedRoles)}. Use apenas: {string.Join(", ", AppRoles.All)}.",
"unsupported_roles");
}
if (request.ClientCredentialsOnly)
{
if (tenant.IsSystem)
{
return await RejectAsync(
tenantId,
StatusCodes.Status400BadRequest,
"Credenciais de cliente não podem ser criadas no SystemTenant.",
"invalid_client_credentials_on_system_tenant");
}
if (normalizedRoles.Count != 1 || !normalizedRoles.Contains(AppRoles.Cliente, StringComparer.OrdinalIgnoreCase))
{
return await RejectAsync(
tenantId,
StatusCodes.Status400BadRequest,
"Neste fluxo, somente a role cliente é permitida.",
"invalid_roles_for_client_credentials_flow");
}
}
if (!tenant.IsSystem && normalizedRoles.Contains(SystemTenantConstants.SystemRole))
{
return await RejectAsync(
tenantId,
StatusCodes.Status400BadRequest,
"A role sysadmin só pode ser usada no SystemTenant.",
"invalid_sysadmin_outside_system_tenant");
}
if (tenant.IsSystem && normalizedRoles.Any(r => r != SystemTenantConstants.SystemRole))
{
return await RejectAsync(
tenantId,
StatusCodes.Status400BadRequest,
"No SystemTenant é permitido apenas a role sysadmin.",
"invalid_non_system_role_for_system_tenant");
}
foreach (var role in normalizedRoles)
{
if (!await _roleManager.RoleExistsAsync(role))
{
return await RejectAsync(
tenantId,
StatusCodes.Status400BadRequest,
$"Role inexistente: {role}",
"role_not_found");
}
}
var email = request.Email.Trim().ToLowerInvariant();
var normalizedEmail = _userManager.NormalizeEmail(email);
var alreadyExists = await _userManager.Users
.IgnoreQueryFilters()
.AnyAsync(u => u.TenantId == tenantId && u.NormalizedEmail == normalizedEmail);
if (alreadyExists)
{
return await RejectAsync(tenantId, StatusCodes.Status409Conflict, "Já existe usuário com este email neste tenant.", "email_exists");
}
var name = string.IsNullOrWhiteSpace(request.Name)
? email
: request.Name.Trim();
var user = new ApplicationUser
{
Name = name,
Email = email,
UserName = email,
TenantId = tenantId,
EmailConfirmed = true,
IsActive = true,
LockoutEnabled = true
};
IdentityResult createResult;
try
{
createResult = await _userManager.CreateAsync(user, request.Password);
}
catch (DbUpdateException)
{
return await RejectAsync(
tenantId,
StatusCodes.Status409Conflict,
"Não foi possível criar usuário. Email/username já em uso.",
"db_conflict");
}
if (!createResult.Succeeded)
{
await _systemAuditService.LogAsync(
action: SystemAuditActions.CreateTenantUserRejected,
targetTenantId: tenantId,
metadata: new
{
reason = "identity_create_failed",
email,
errors = createResult.Errors.Select(e => e.Description).ToList()
});
return BadRequest(createResult.Errors.Select(e => e.Description).ToList());
}
var addRolesResult = await _userManager.AddToRolesAsync(user, normalizedRoles);
if (!addRolesResult.Succeeded)
{
await _userManager.DeleteAsync(user);
await _systemAuditService.LogAsync(
action: SystemAuditActions.CreateTenantUserRejected,
targetTenantId: tenantId,
metadata: new
{
reason = "identity_add_roles_failed",
email,
roles = normalizedRoles,
errors = addRolesResult.Errors.Select(e => e.Description).ToList()
});
return BadRequest(addRolesResult.Errors.Select(e => e.Description).ToList());
}
var linesReassigned = 0;
if (request.ClientCredentialsOnly)
{
linesReassigned = await RebindMobileLinesToTenantBySourceKeyAsync(tenant);
}
await _systemAuditService.LogAsync(
action: SystemAuditActions.CreateTenantUser,
targetTenantId: tenantId,
metadata: new
{
createdUserId = user.Id,
email,
roles = normalizedRoles,
linesReassigned
});
var response = new SystemTenantUserCreatedDto
{
UserId = user.Id,
TenantId = tenantId,
Email = email,
Roles = normalizedRoles
};
return StatusCode(StatusCodes.Status201Created, response);
}
private async Task<ActionResult<SystemTenantUserCreatedDto>> RejectAsync(
Guid targetTenantId,
int statusCode,
string message,
string reason)
{
await _systemAuditService.LogAsync(
action: SystemAuditActions.CreateTenantUserRejected,
targetTenantId: targetTenantId,
metadata: new { reason, message });
return StatusCode(statusCode, message);
}
private async Task<int> RebindMobileLinesToTenantBySourceKeyAsync(Tenant tenant)
{
if (tenant.Id == Guid.Empty)
{
return 0;
}
if (!string.Equals(
tenant.SourceType,
SystemTenantConstants.MobileLinesClienteSourceType,
StringComparison.OrdinalIgnoreCase))
{
return 0;
}
var normalizedKeys = new HashSet<string>(StringComparer.Ordinal);
AddNormalizedTenantKey(normalizedKeys, tenant.SourceKey);
AddNormalizedTenantKey(normalizedKeys, tenant.NomeOficial);
if (normalizedKeys.Count == 0)
{
return 0;
}
var candidates = await _db.MobileLines
.IgnoreQueryFilters()
.Where(x => x.TenantId != tenant.Id)
.Where(x => x.Cliente != null && x.Cliente != string.Empty)
.ToListAsync();
if (candidates.Count == 0)
{
return 0;
}
var hasAnyTenantLine = await _db.MobileLines
.IgnoreQueryFilters()
.AsNoTracking()
.AnyAsync(x => x.TenantId == tenant.Id);
var now = DateTime.UtcNow;
var reassigned = ReassignByMatcher(
candidates,
normalizedKeys,
tenant.Id,
now,
isRelaxedMatch: false);
if (reassigned == 0 && !hasAnyTenantLine)
{
reassigned = ReassignByMatcher(
candidates,
normalizedKeys,
tenant.Id,
now,
isRelaxedMatch: true);
}
if (reassigned > 0)
{
await _db.SaveChangesAsync();
}
return reassigned;
}
private static int ReassignByMatcher(
IReadOnlyList<MobileLine> candidates,
IReadOnlyCollection<string> normalizedKeys,
Guid tenantId,
DateTime now,
bool isRelaxedMatch)
{
if (normalizedKeys.Count == 0)
{
return 0;
}
var keys = isRelaxedMatch
? normalizedKeys.Where(k => k.Length >= 6).ToList()
: normalizedKeys.ToList();
if (keys.Count == 0)
{
return 0;
}
var reassigned = 0;
foreach (var line in candidates)
{
var normalizedClient = NormalizeTenantKey(line.Cliente ?? string.Empty);
if (string.IsNullOrWhiteSpace(normalizedClient) ||
string.Equals(normalizedClient, "RESERVA", StringComparison.Ordinal))
{
continue;
}
var matches = !isRelaxedMatch
? keys.Contains(normalizedClient, StringComparer.Ordinal)
: keys.Any(k =>
normalizedClient.Contains(k, StringComparison.Ordinal) ||
k.Contains(normalizedClient, StringComparison.Ordinal));
if (!matches)
{
continue;
}
line.TenantId = tenantId;
line.UpdatedAt = now;
reassigned++;
}
return reassigned;
}
private static void AddNormalizedTenantKey(ISet<string> keys, string? rawKey)
{
var normalized = NormalizeTenantKey(rawKey ?? string.Empty);
if (string.IsNullOrWhiteSpace(normalized))
{
return;
}
if (string.Equals(normalized, "RESERVA", StringComparison.Ordinal))
{
return;
}
keys.Add(normalized);
}
private static string NormalizeTenantKey(string value)
{
if (string.IsNullOrWhiteSpace(value))
{
return string.Empty;
}
var normalized = value.Trim().Normalize(NormalizationForm.FormD);
var sb = new StringBuilder(normalized.Length);
var previousWasSpace = false;
foreach (var ch in normalized)
{
var category = CharUnicodeInfo.GetUnicodeCategory(ch);
if (category == UnicodeCategory.NonSpacingMark)
{
continue;
}
if (char.IsWhiteSpace(ch))
{
if (!previousWasSpace)
{
sb.Append(' ');
previousWasSpace = true;
}
continue;
}
sb.Append(char.ToUpperInvariant(ch));
previousWasSpace = false;
}
return sb.ToString().Trim();
}
}

View File

@ -1,56 +0,0 @@
using line_gestao_api.Data;
using line_gestao_api.Dtos;
using line_gestao_api.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace line_gestao_api.Controllers;
[ApiController]
[Route("api/system/tenants")]
[Authorize(Policy = "SystemAdmin")]
public class SystemTenantsController : ControllerBase
{
private readonly AppDbContext _db;
private readonly ISystemAuditService _systemAuditService;
public SystemTenantsController(AppDbContext db, ISystemAuditService systemAuditService)
{
_db = db;
_systemAuditService = systemAuditService;
}
[HttpGet]
public async Task<ActionResult<IReadOnlyList<SystemTenantListItemDto>>> GetTenants(
[FromQuery] string source = SystemTenantConstants.MobileLinesClienteSourceType,
[FromQuery] bool active = true)
{
var query = _db.Tenants
.AsNoTracking()
.Where(t => !t.IsSystem);
if (!string.IsNullOrWhiteSpace(source))
{
query = query.Where(t => t.SourceType == source);
}
query = query.Where(t => t.Ativo == active);
var tenants = await query
.OrderBy(t => t.NomeOficial)
.Select(t => new SystemTenantListItemDto
{
TenantId = t.Id,
NomeOficial = t.NomeOficial
})
.ToListAsync();
await _systemAuditService.LogAsync(
action: SystemAuditActions.ListTenants,
targetTenantId: SystemTenantConstants.SystemTenantId,
metadata: new { source, active, returnedCount = tenants.Count });
return Ok(tenants);
}
}

View File

@ -1,33 +0,0 @@
using line_gestao_api.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace line_gestao_api.Controllers
{
[ApiController]
[Route("api/templates")]
[Authorize(Roles = "sysadmin,gestor")]
public class TemplatesController : ControllerBase
{
private readonly GeralSpreadsheetTemplateService _geralSpreadsheetTemplateService;
public TemplatesController(GeralSpreadsheetTemplateService geralSpreadsheetTemplateService)
{
_geralSpreadsheetTemplateService = geralSpreadsheetTemplateService;
}
[HttpGet("planilha-geral")]
public IActionResult DownloadPlanilhaGeral()
{
Response.Headers["Cache-Control"] = "no-store, no-cache, must-revalidate";
Response.Headers["Pragma"] = "no-cache";
Response.Headers["Expires"] = "0";
var bytes = _geralSpreadsheetTemplateService.BuildPlanilhaGeralTemplate();
return File(
bytes,
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"MODELO_GERAL_LINEGESTAO.xlsx");
}
}
}

View File

@ -112,7 +112,7 @@ namespace line_gestao_api.Controllers
// ✅ CREATE // ✅ CREATE
// ========================================================== // ==========================================================
[HttpPost] [HttpPost]
[Authorize(Roles = "sysadmin,gestor")] [Authorize(Roles = "admin,gestor")]
public async Task<ActionResult<TrocaNumeroDetailDto>> Create([FromBody] CreateTrocaNumeroDto req) public async Task<ActionResult<TrocaNumeroDetailDto>> Create([FromBody] CreateTrocaNumeroDto req)
{ {
var now = DateTime.UtcNow; var now = DateTime.UtcNow;
@ -141,7 +141,7 @@ namespace line_gestao_api.Controllers
// ✅ UPDATE // ✅ UPDATE
// ========================================================== // ==========================================================
[HttpPut("{id:guid}")] [HttpPut("{id:guid}")]
[Authorize(Roles = "sysadmin,gestor")] [Authorize(Roles = "admin,gestor")]
public async Task<IActionResult> Update(Guid id, [FromBody] UpdateTrocaNumeroRequest req) public async Task<IActionResult> Update(Guid id, [FromBody] UpdateTrocaNumeroRequest req)
{ {
var x = await _db.TrocaNumeroLines.FirstOrDefaultAsync(a => a.Id == id); var x = await _db.TrocaNumeroLines.FirstOrDefaultAsync(a => a.Id == id);
@ -167,7 +167,7 @@ namespace line_gestao_api.Controllers
// ✅ DELETE // ✅ DELETE
// ========================================================== // ==========================================================
[HttpDelete("{id:guid}")] [HttpDelete("{id:guid}")]
[Authorize(Roles = "sysadmin")] [Authorize(Roles = "admin")]
public async Task<IActionResult> Delete(Guid id) public async Task<IActionResult> Delete(Guid id)
{ {
var x = await _db.TrocaNumeroLines.FirstOrDefaultAsync(a => a.Id == id); var x = await _db.TrocaNumeroLines.FirstOrDefaultAsync(a => a.Id == id);

View File

@ -56,7 +56,6 @@ namespace line_gestao_api.Controllers
var searchDateEndUtc = hasDateSearch ? searchDateStartUtc.AddDays(1) : DateTime.MinValue; var searchDateEndUtc = hasDateSearch ? searchDateStartUtc.AddDays(1) : DateTime.MinValue;
q = q.Where(x => q = q.Where(x =>
EF.Functions.ILike(x.Linha ?? "", $"%{s}%") || EF.Functions.ILike(x.Linha ?? "", $"%{s}%") ||
(x.Linha != null && _db.MobileLines.Any(m => m.Linha == x.Linha && EF.Functions.ILike(m.Chip ?? "", $"%{s}%"))) ||
EF.Functions.ILike(x.Cliente ?? "", $"%{s}%") || EF.Functions.ILike(x.Cliente ?? "", $"%{s}%") ||
EF.Functions.ILike(x.TipoPessoa ?? "", $"%{s}%") || EF.Functions.ILike(x.TipoPessoa ?? "", $"%{s}%") ||
EF.Functions.ILike(x.Nome ?? "", $"%{s}%") || EF.Functions.ILike(x.Nome ?? "", $"%{s}%") ||
@ -152,7 +151,6 @@ namespace line_gestao_api.Controllers
q = q.Where(x => q = q.Where(x =>
EF.Functions.ILike(x.Cliente ?? "", $"%{s}%") || EF.Functions.ILike(x.Cliente ?? "", $"%{s}%") ||
EF.Functions.ILike(x.Linha ?? "", $"%{s}%") || EF.Functions.ILike(x.Linha ?? "", $"%{s}%") ||
(x.Linha != null && _db.MobileLines.Any(m => m.Linha == x.Linha && EF.Functions.ILike(m.Chip ?? "", $"%{s}%"))) ||
EF.Functions.ILike(x.TipoPessoa ?? "", $"%{s}%") || EF.Functions.ILike(x.TipoPessoa ?? "", $"%{s}%") ||
EF.Functions.ILike(x.Nome ?? "", $"%{s}%") || EF.Functions.ILike(x.Nome ?? "", $"%{s}%") ||
EF.Functions.ILike(x.RazaoSocial ?? "", $"%{s}%") || EF.Functions.ILike(x.RazaoSocial ?? "", $"%{s}%") ||
@ -263,7 +261,7 @@ namespace line_gestao_api.Controllers
} }
[HttpPost] [HttpPost]
[Authorize(Roles = "sysadmin")] [Authorize(Roles = "admin")]
public async Task<ActionResult<UserDataDetailDto>> Create([FromBody] CreateUserDataRequest req) public async Task<ActionResult<UserDataDetailDto>> Create([FromBody] CreateUserDataRequest req)
{ {
var now = DateTime.UtcNow; var now = DateTime.UtcNow;
@ -365,7 +363,7 @@ namespace line_gestao_api.Controllers
} }
[HttpPut("{id:guid}")] [HttpPut("{id:guid}")]
[Authorize(Roles = "sysadmin")] [Authorize(Roles = "admin")]
public async Task<IActionResult> Update(Guid id, [FromBody] UpdateUserDataRequest req) public async Task<IActionResult> Update(Guid id, [FromBody] UpdateUserDataRequest req)
{ {
var x = await _db.UserDatas.FirstOrDefaultAsync(a => a.Id == id); var x = await _db.UserDatas.FirstOrDefaultAsync(a => a.Id == id);
@ -397,7 +395,7 @@ namespace line_gestao_api.Controllers
} }
[HttpDelete("{id:guid}")] [HttpDelete("{id:guid}")]
[Authorize(Roles = "sysadmin")] [Authorize(Roles = "admin")]
public async Task<IActionResult> Delete(Guid id) public async Task<IActionResult> Delete(Guid id)
{ {
var x = await _db.UserDatas.FirstOrDefaultAsync(a => a.Id == id); var x = await _db.UserDatas.FirstOrDefaultAsync(a => a.Id == id);

View File

@ -17,9 +17,8 @@ public class UsersController : ControllerBase
{ {
private static readonly HashSet<string> AllowedRoles = new(StringComparer.OrdinalIgnoreCase) private static readonly HashSet<string> AllowedRoles = new(StringComparer.OrdinalIgnoreCase)
{ {
AppRoles.SysAdmin, "admin",
AppRoles.Gestor, "gestor"
AppRoles.Cliente
}; };
private readonly AppDbContext _db; private readonly AppDbContext _db;
@ -40,7 +39,7 @@ public class UsersController : ControllerBase
} }
[HttpPost] [HttpPost]
[Authorize(Roles = "sysadmin")] [Authorize(Roles = "admin")]
public async Task<ActionResult<UserListItemDto>> Create([FromBody] UserCreateRequest req) public async Task<ActionResult<UserListItemDto>> Create([FromBody] UserCreateRequest req)
{ {
var errors = ValidateCreate(req); var errors = ValidateCreate(req);
@ -123,7 +122,7 @@ public class UsersController : ControllerBase
} }
[HttpGet] [HttpGet]
[Authorize(Roles = "sysadmin")] [Authorize(Roles = "admin")]
public async Task<ActionResult<PagedResult<UserListItemDto>>> GetAll( public async Task<ActionResult<PagedResult<UserListItemDto>>> GetAll(
[FromQuery] string? search, [FromQuery] string? search,
[FromQuery] string? permissao, [FromQuery] string? permissao,
@ -133,9 +132,7 @@ public class UsersController : ControllerBase
page = page < 1 ? 1 : page; page = page < 1 ? 1 : page;
pageSize = pageSize < 1 ? 20 : pageSize; pageSize = pageSize < 1 ? 20 : pageSize;
var usersQuery = _userManager.Users var usersQuery = _userManager.Users.AsNoTracking();
.IgnoreQueryFilters()
.AsNoTracking();
if (!string.IsNullOrWhiteSpace(search)) if (!string.IsNullOrWhiteSpace(search))
{ {
@ -194,13 +191,10 @@ public class UsersController : ControllerBase
} }
[HttpGet("{id:guid}")] [HttpGet("{id:guid}")]
[Authorize(Roles = "sysadmin")] [Authorize(Roles = "admin")]
public async Task<ActionResult<UserListItemDto>> GetById(Guid id) public async Task<ActionResult<UserListItemDto>> GetById(Guid id)
{ {
var user = await _userManager.Users var user = await _userManager.Users.AsNoTracking().FirstOrDefaultAsync(u => u.Id == id);
.IgnoreQueryFilters()
.AsNoTracking()
.FirstOrDefaultAsync(u => u.Id == id);
if (user == null) if (user == null)
{ {
return NotFound(); return NotFound();
@ -221,7 +215,7 @@ public class UsersController : ControllerBase
} }
[HttpPatch("{id:guid}")] [HttpPatch("{id:guid}")]
[Authorize(Roles = "sysadmin")] [Authorize(Roles = "admin")]
public async Task<IActionResult> Update(Guid id, [FromBody] UserUpdateRequest req) public async Task<IActionResult> Update(Guid id, [FromBody] UserUpdateRequest req)
{ {
var errors = await ValidateUpdateAsync(id, req); var errors = await ValidateUpdateAsync(id, req);
@ -230,9 +224,7 @@ public class UsersController : ControllerBase
return BadRequest(new ValidationErrorResponse { Errors = errors }); return BadRequest(new ValidationErrorResponse { Errors = errors });
} }
var user = await _userManager.Users var user = await _userManager.Users.FirstOrDefaultAsync(u => u.Id == id);
.IgnoreQueryFilters()
.FirstOrDefaultAsync(u => u.Id == id);
if (user == null) if (user == null)
{ {
return NotFound(); return NotFound();
@ -303,9 +295,14 @@ public class UsersController : ControllerBase
} }
[HttpDelete("{id:guid}")] [HttpDelete("{id:guid}")]
[Authorize(Roles = "sysadmin")] [Authorize(Roles = "admin")]
public async Task<IActionResult> Delete(Guid id) public async Task<IActionResult> Delete(Guid id)
{ {
if (_tenantProvider.TenantId == null)
{
return Unauthorized();
}
var currentUserId = GetCurrentUserId(); var currentUserId = GetCurrentUserId();
if (currentUserId.HasValue && currentUserId.Value == id) if (currentUserId.HasValue && currentUserId.Value == id)
{ {
@ -318,14 +315,12 @@ public class UsersController : ControllerBase
}); });
} }
var user = await _userManager.Users var tenantId = _tenantProvider.TenantId.Value;
.IgnoreQueryFilters() var user = await _userManager.Users.FirstOrDefaultAsync(u => u.Id == id && u.TenantId == tenantId);
.FirstOrDefaultAsync(u => u.Id == id);
if (user == null) if (user == null)
{ {
return NotFound(); return NotFound();
} }
var tenantId = user.TenantId;
if (user.IsActive) if (user.IsActive)
{ {
@ -339,12 +334,12 @@ public class UsersController : ControllerBase
} }
var targetRoles = await _userManager.GetRolesAsync(user); var targetRoles = await _userManager.GetRolesAsync(user);
var isAdmin = targetRoles.Any(r => string.Equals(r, AppRoles.SysAdmin, StringComparison.OrdinalIgnoreCase)); var isAdmin = targetRoles.Any(r => string.Equals(r, "admin", StringComparison.OrdinalIgnoreCase));
if (isAdmin) if (isAdmin)
{ {
var adminRoleId = await _roleManager.Roles var adminRoleId = await _roleManager.Roles
.Where(r => r.Name == AppRoles.SysAdmin) .Where(r => r.Name == "admin")
.Select(r => (Guid?)r.Id) .Select(r => (Guid?)r.Id)
.FirstOrDefaultAsync(); .FirstOrDefaultAsync();
@ -365,7 +360,7 @@ public class UsersController : ControllerBase
{ {
Errors = new List<ValidationErrorDto> Errors = new List<ValidationErrorDto>
{ {
new() { Field = "usuario", Message = "Não é permitido excluir o último sysadmin." } new() { Field = "usuario", Message = "Não é permitido excluir o último administrador." }
} }
}); });
} }
@ -427,10 +422,6 @@ public class UsersController : ControllerBase
private async Task<List<ValidationErrorDto>> ValidateUpdateAsync(Guid userId, UserUpdateRequest req) private async Task<List<ValidationErrorDto>> ValidateUpdateAsync(Guid userId, UserUpdateRequest req)
{ {
var errors = new List<ValidationErrorDto>(); var errors = new List<ValidationErrorDto>();
var targetUser = await _userManager.Users
.IgnoreQueryFilters()
.AsNoTracking()
.FirstOrDefaultAsync(u => u.Id == userId);
if (!string.IsNullOrWhiteSpace(req.Nome) && req.Nome.Trim().Length < 2) if (!string.IsNullOrWhiteSpace(req.Nome) && req.Nome.Trim().Length < 2)
{ {
@ -442,22 +433,22 @@ public class UsersController : ControllerBase
var email = req.Email.Trim().ToLowerInvariant(); var email = req.Email.Trim().ToLowerInvariant();
var normalized = _userManager.NormalizeEmail(email); var normalized = _userManager.NormalizeEmail(email);
if (targetUser == null) var tenantId = _tenantProvider.TenantId;
if (tenantId == null)
{ {
return errors; errors.Add(new ValidationErrorDto { Field = "email", Message = "Tenant inválido." });
} }
else
var tenantId = targetUser.TenantId;
var exists = await _userManager.Users
.IgnoreQueryFilters()
.AnyAsync(u =>
u.Id != userId &&
u.TenantId == tenantId &&
u.NormalizedEmail == normalized);
if (exists)
{ {
errors.Add(new ValidationErrorDto { Field = "email", Message = "E-mail já cadastrado." }); var exists = await _userManager.Users.AnyAsync(u =>
u.Id != userId &&
u.TenantId == tenantId &&
u.NormalizedEmail == normalized);
if (exists)
{
errors.Add(new ValidationErrorDto { Field = "email", Message = "E-mail já cadastrado." });
}
} }
} }

View File

@ -56,7 +56,6 @@ namespace line_gestao_api.Controllers
q = q.Where(x => q = q.Where(x =>
EF.Functions.ILike(x.Conta ?? "", $"%{s}%") || EF.Functions.ILike(x.Conta ?? "", $"%{s}%") ||
EF.Functions.ILike(x.Linha ?? "", $"%{s}%") || EF.Functions.ILike(x.Linha ?? "", $"%{s}%") ||
(x.Linha != null && _db.MobileLines.Any(m => m.Linha == x.Linha && EF.Functions.ILike(m.Chip ?? "", $"%{s}%"))) ||
EF.Functions.ILike(x.Cliente ?? "", $"%{s}%") || EF.Functions.ILike(x.Cliente ?? "", $"%{s}%") ||
EF.Functions.ILike(x.Usuario ?? "", $"%{s}%") || EF.Functions.ILike(x.Usuario ?? "", $"%{s}%") ||
EF.Functions.ILike(x.PlanoContrato ?? "", $"%{s}%") || EF.Functions.ILike(x.PlanoContrato ?? "", $"%{s}%") ||
@ -99,10 +98,6 @@ namespace line_gestao_api.Controllers
PlanoContrato = x.PlanoContrato, PlanoContrato = x.PlanoContrato,
DtEfetivacaoServico = x.DtEfetivacaoServico, DtEfetivacaoServico = x.DtEfetivacaoServico,
DtTerminoFidelizacao = x.DtTerminoFidelizacao, DtTerminoFidelizacao = x.DtTerminoFidelizacao,
AutoRenewYears = x.AutoRenewYears,
AutoRenewReferenceEndDate = x.AutoRenewReferenceEndDate,
AutoRenewConfiguredAt = x.AutoRenewConfiguredAt,
LastAutoRenewedAt = x.LastAutoRenewedAt,
Total = x.Total Total = x.Total
}) })
.ToListAsync(); .ToListAsync();
@ -147,7 +142,6 @@ namespace line_gestao_api.Controllers
q = q.Where(x => q = q.Where(x =>
EF.Functions.ILike(x.Conta ?? "", $"%{s}%") || EF.Functions.ILike(x.Conta ?? "", $"%{s}%") ||
EF.Functions.ILike(x.Linha ?? "", $"%{s}%") || EF.Functions.ILike(x.Linha ?? "", $"%{s}%") ||
(x.Linha != null && _db.MobileLines.Any(m => m.Linha == x.Linha && EF.Functions.ILike(m.Chip ?? "", $"%{s}%"))) ||
EF.Functions.ILike(x.Cliente ?? "", $"%{s}%") || EF.Functions.ILike(x.Cliente ?? "", $"%{s}%") ||
EF.Functions.ILike(x.Usuario ?? "", $"%{s}%") || EF.Functions.ILike(x.Usuario ?? "", $"%{s}%") ||
EF.Functions.ILike(x.PlanoContrato ?? "", $"%{s}%") || EF.Functions.ILike(x.PlanoContrato ?? "", $"%{s}%") ||
@ -245,10 +239,6 @@ namespace line_gestao_api.Controllers
PlanoContrato = x.PlanoContrato, PlanoContrato = x.PlanoContrato,
DtEfetivacaoServico = x.DtEfetivacaoServico, DtEfetivacaoServico = x.DtEfetivacaoServico,
DtTerminoFidelizacao = x.DtTerminoFidelizacao, DtTerminoFidelizacao = x.DtTerminoFidelizacao,
AutoRenewYears = x.AutoRenewYears,
AutoRenewReferenceEndDate = x.AutoRenewReferenceEndDate,
AutoRenewConfiguredAt = x.AutoRenewConfiguredAt,
LastAutoRenewedAt = x.LastAutoRenewedAt,
Total = x.Total, Total = x.Total,
CreatedAt = x.CreatedAt, CreatedAt = x.CreatedAt,
UpdatedAt = x.UpdatedAt UpdatedAt = x.UpdatedAt
@ -256,7 +246,7 @@ namespace line_gestao_api.Controllers
} }
[HttpPost] [HttpPost]
[Authorize(Roles = "sysadmin")] [Authorize(Roles = "admin")]
public async Task<ActionResult<VigenciaLineDetailDto>> Create([FromBody] CreateVigenciaRequest req) public async Task<ActionResult<VigenciaLineDetailDto>> Create([FromBody] CreateVigenciaRequest req)
{ {
var now = DateTime.UtcNow; var now = DateTime.UtcNow;
@ -343,10 +333,6 @@ namespace line_gestao_api.Controllers
PlanoContrato = e.PlanoContrato, PlanoContrato = e.PlanoContrato,
DtEfetivacaoServico = e.DtEfetivacaoServico, DtEfetivacaoServico = e.DtEfetivacaoServico,
DtTerminoFidelizacao = e.DtTerminoFidelizacao, DtTerminoFidelizacao = e.DtTerminoFidelizacao,
AutoRenewYears = e.AutoRenewYears,
AutoRenewReferenceEndDate = e.AutoRenewReferenceEndDate,
AutoRenewConfiguredAt = e.AutoRenewConfiguredAt,
LastAutoRenewedAt = e.LastAutoRenewedAt,
Total = e.Total, Total = e.Total,
CreatedAt = e.CreatedAt, CreatedAt = e.CreatedAt,
UpdatedAt = e.UpdatedAt UpdatedAt = e.UpdatedAt
@ -354,15 +340,12 @@ namespace line_gestao_api.Controllers
} }
[HttpPut("{id:guid}")] [HttpPut("{id:guid}")]
[Authorize(Roles = "sysadmin")] [Authorize(Roles = "admin")]
public async Task<IActionResult> Update(Guid id, [FromBody] UpdateVigenciaRequest req) public async Task<IActionResult> Update(Guid id, [FromBody] UpdateVigenciaRequest req)
{ {
var x = await _db.VigenciaLines.FirstOrDefaultAsync(a => a.Id == id); var x = await _db.VigenciaLines.FirstOrDefaultAsync(a => a.Id == id);
if (x == null) return NotFound(); if (x == null) return NotFound();
var previousEfetivacao = x.DtEfetivacaoServico;
var previousTermino = x.DtTerminoFidelizacao;
if (req.Item.HasValue) x.Item = req.Item.Value; if (req.Item.HasValue) x.Item = req.Item.Value;
if (req.Conta != null) x.Conta = TrimOrNull(req.Conta); if (req.Conta != null) x.Conta = TrimOrNull(req.Conta);
if (req.Linha != null) x.Linha = TrimOrNull(req.Linha); if (req.Linha != null) x.Linha = TrimOrNull(req.Linha);
@ -375,13 +358,6 @@ namespace line_gestao_api.Controllers
if (req.Total.HasValue) x.Total = req.Total.Value; if (req.Total.HasValue) x.Total = req.Total.Value;
var efetivacaoChanged = req.DtEfetivacaoServico.HasValue && !IsSameUtcDate(previousEfetivacao, x.DtEfetivacaoServico);
var terminoChanged = req.DtTerminoFidelizacao.HasValue && !IsSameUtcDate(previousTermino, x.DtTerminoFidelizacao);
if (efetivacaoChanged || terminoChanged)
{
ClearAutoRenewSchedule(x);
}
x.UpdatedAt = DateTime.UtcNow; x.UpdatedAt = DateTime.UtcNow;
await _db.SaveChangesAsync(); await _db.SaveChangesAsync();
@ -389,41 +365,8 @@ namespace line_gestao_api.Controllers
return NoContent(); return NoContent();
} }
[HttpPost("{id:guid}/renew")]
public async Task<IActionResult> ConfigureAutoRenew(Guid id, [FromBody] ConfigureVigenciaRenewalRequest req)
{
if (req.Years != 2)
{
return BadRequest(new { message = "A renovação automática permite somente 2 anos." });
}
var x = await _db.VigenciaLines.FirstOrDefaultAsync(a => a.Id == id);
if (x == null) return NotFound();
if (!x.DtTerminoFidelizacao.HasValue)
{
return BadRequest(new { message = "A linha não possui data de término de fidelização para programar renovação." });
}
var now = DateTime.UtcNow;
x.AutoRenewYears = 2;
x.AutoRenewReferenceEndDate = DateTime.SpecifyKind(x.DtTerminoFidelizacao.Value.Date, DateTimeKind.Utc);
x.AutoRenewConfiguredAt = now;
x.UpdatedAt = now;
await _db.SaveChangesAsync();
await _vigenciaNotificationSyncService.SyncCurrentTenantAsync();
return Ok(new
{
message = "Renovação automática de +2 anos programada para o vencimento.",
autoRenewYears = x.AutoRenewYears,
autoRenewReferenceEndDate = x.AutoRenewReferenceEndDate
});
}
[HttpDelete("{id:guid}")] [HttpDelete("{id:guid}")]
[Authorize(Roles = "sysadmin")] [Authorize(Roles = "admin")]
public async Task<IActionResult> Delete(Guid id) public async Task<IActionResult> Delete(Guid id)
{ {
var x = await _db.VigenciaLines.FirstOrDefaultAsync(a => a.Id == id); var x = await _db.VigenciaLines.FirstOrDefaultAsync(a => a.Id == id);
@ -447,20 +390,6 @@ namespace line_gestao_api.Controllers
: (dt.Kind == DateTimeKind.Local ? dt.ToUniversalTime() : DateTime.SpecifyKind(dt, DateTimeKind.Utc)); : (dt.Kind == DateTimeKind.Local ? dt.ToUniversalTime() : DateTime.SpecifyKind(dt, DateTimeKind.Utc));
} }
private static void ClearAutoRenewSchedule(VigenciaLine line)
{
line.AutoRenewYears = null;
line.AutoRenewReferenceEndDate = null;
line.AutoRenewConfiguredAt = null;
}
private static bool IsSameUtcDate(DateTime? a, DateTime? b)
{
if (!a.HasValue && !b.HasValue) return true;
if (!a.HasValue || !b.HasValue) return false;
return DateTime.SpecifyKind(a.Value.Date, DateTimeKind.Utc) == DateTime.SpecifyKind(b.Value.Date, DateTimeKind.Utc);
}
private static string OnlyDigits(string? s) private static string OnlyDigits(string? s)
{ {
if (string.IsNullOrWhiteSpace(s)) return ""; if (string.IsNullOrWhiteSpace(s)) return "";

View File

@ -78,15 +78,6 @@ public class AppDbContext : IdentityDbContext<ApplicationUser, IdentityRole<Guid
{ {
base.OnModelCreating(modelBuilder); base.OnModelCreating(modelBuilder);
modelBuilder.Entity<Tenant>(e =>
{
e.Property(x => x.NomeOficial).HasMaxLength(200);
e.Property(x => x.SourceType).HasMaxLength(80);
e.Property(x => x.SourceKey).HasMaxLength(300);
e.HasIndex(x => new { x.SourceType, x.SourceKey }).IsUnique();
e.HasIndex(x => new { x.IsSystem, x.Ativo });
});
// ========================= // =========================
// ✅ USER (Identity) // ✅ USER (Identity)
// ========================= // =========================
@ -179,7 +170,6 @@ public class AppDbContext : IdentityDbContext<ApplicationUser, IdentityRole<Guid
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.AutoRenewReferenceEndDate);
e.HasIndex(x => x.TenantId); e.HasIndex(x => x.TenantId);
}); });
@ -280,7 +270,6 @@ public class AppDbContext : IdentityDbContext<ApplicationUser, IdentityRole<Guid
// ========================= // =========================
modelBuilder.Entity<AuditLog>(e => modelBuilder.Entity<AuditLog>(e =>
{ {
e.Property(x => x.MetadataJson).HasColumnType("jsonb");
e.Property(x => x.Action).HasMaxLength(20); e.Property(x => x.Action).HasMaxLength(20);
e.Property(x => x.Page).HasMaxLength(80); e.Property(x => x.Page).HasMaxLength(80);
e.Property(x => x.EntityName).HasMaxLength(120); e.Property(x => x.EntityName).HasMaxLength(120);
@ -292,13 +281,8 @@ public class AppDbContext : IdentityDbContext<ApplicationUser, IdentityRole<Guid
e.Property(x => x.RequestMethod).HasMaxLength(10); e.Property(x => x.RequestMethod).HasMaxLength(10);
e.Property(x => x.IpAddress).HasMaxLength(80); e.Property(x => x.IpAddress).HasMaxLength(80);
e.Property(x => x.ChangesJson).HasColumnType("jsonb"); e.Property(x => x.ChangesJson).HasColumnType("jsonb");
e.Property(x => x.ActorTenantId);
e.Property(x => x.TargetTenantId);
e.HasIndex(x => x.TenantId); e.HasIndex(x => x.TenantId);
e.HasIndex(x => x.ActorTenantId);
e.HasIndex(x => x.TargetTenantId);
e.HasIndex(x => x.ActorUserId);
e.HasIndex(x => x.OccurredAtUtc); e.HasIndex(x => x.OccurredAtUtc);
e.HasIndex(x => x.Page); e.HasIndex(x => x.Page);
e.HasIndex(x => x.UserId); e.HasIndex(x => x.UserId);
@ -334,33 +318,33 @@ public class AppDbContext : IdentityDbContext<ApplicationUser, IdentityRole<Guid
.OnDelete(DeleteBehavior.Cascade); .OnDelete(DeleteBehavior.Cascade);
}); });
modelBuilder.Entity<MobileLine>().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId)); modelBuilder.Entity<MobileLine>().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId);
modelBuilder.Entity<MuregLine>().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId)); modelBuilder.Entity<MuregLine>().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId);
modelBuilder.Entity<BillingClient>().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId)); modelBuilder.Entity<BillingClient>().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId);
modelBuilder.Entity<UserData>().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId)); modelBuilder.Entity<UserData>().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId);
modelBuilder.Entity<VigenciaLine>().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId)); modelBuilder.Entity<VigenciaLine>().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId);
modelBuilder.Entity<TrocaNumeroLine>().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId)); modelBuilder.Entity<TrocaNumeroLine>().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId);
modelBuilder.Entity<ChipVirgemLine>().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId)); modelBuilder.Entity<ChipVirgemLine>().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId);
modelBuilder.Entity<ControleRecebidoLine>().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId)); modelBuilder.Entity<ControleRecebidoLine>().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId);
modelBuilder.Entity<Notification>().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId)); modelBuilder.Entity<Notification>().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId);
modelBuilder.Entity<ResumoMacrophonyPlan>().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId)); modelBuilder.Entity<ResumoMacrophonyPlan>().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId);
modelBuilder.Entity<ResumoMacrophonyTotal>().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId)); modelBuilder.Entity<ResumoMacrophonyTotal>().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId);
modelBuilder.Entity<ResumoVivoLineResumo>().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId)); modelBuilder.Entity<ResumoVivoLineResumo>().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId);
modelBuilder.Entity<ResumoVivoLineTotal>().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId)); modelBuilder.Entity<ResumoVivoLineTotal>().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId);
modelBuilder.Entity<ResumoClienteEspecial>().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId)); modelBuilder.Entity<ResumoClienteEspecial>().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId);
modelBuilder.Entity<ResumoPlanoContratoResumo>().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId)); modelBuilder.Entity<ResumoPlanoContratoResumo>().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId);
modelBuilder.Entity<ResumoPlanoContratoTotal>().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId)); modelBuilder.Entity<ResumoPlanoContratoTotal>().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId);
modelBuilder.Entity<ResumoLineTotais>().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId)); modelBuilder.Entity<ResumoLineTotais>().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId);
modelBuilder.Entity<ResumoGbDistribuicao>().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId)); modelBuilder.Entity<ResumoGbDistribuicao>().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId);
modelBuilder.Entity<ResumoGbDistribuicaoTotal>().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId)); modelBuilder.Entity<ResumoGbDistribuicaoTotal>().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId);
modelBuilder.Entity<ResumoReservaLine>().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId)); modelBuilder.Entity<ResumoReservaLine>().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId);
modelBuilder.Entity<ResumoReservaTotal>().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId)); modelBuilder.Entity<ResumoReservaTotal>().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId);
modelBuilder.Entity<ParcelamentoLine>().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId)); modelBuilder.Entity<ParcelamentoLine>().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId);
modelBuilder.Entity<ParcelamentoMonthValue>().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId)); modelBuilder.Entity<ParcelamentoMonthValue>().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId);
modelBuilder.Entity<AuditLog>().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId)); modelBuilder.Entity<AuditLog>().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId);
modelBuilder.Entity<ImportAuditRun>().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId)); modelBuilder.Entity<ImportAuditRun>().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId);
modelBuilder.Entity<ImportAuditIssue>().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId)); modelBuilder.Entity<ImportAuditIssue>().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId);
modelBuilder.Entity<ApplicationUser>().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId)); modelBuilder.Entity<ApplicationUser>().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId);
} }
public override int SaveChanges() public override int SaveChanges()
@ -379,12 +363,12 @@ public class AppDbContext : IdentityDbContext<ApplicationUser, IdentityRole<Guid
private void ApplyTenantIds() private void ApplyTenantIds()
{ {
if (_tenantProvider.ActorTenantId == null) if (_tenantProvider.TenantId == null)
{ {
return; return;
} }
var tenantId = _tenantProvider.ActorTenantId.Value; var tenantId = _tenantProvider.TenantId.Value;
foreach (var entry in ChangeTracker.Entries<ITenantEntity>().Where(e => e.State == EntityState.Added)) foreach (var entry in ChangeTracker.Entries<ITenantEntity>().Where(e => e.State == EntityState.Added))
{ {
if (entry.Entity.TenantId == Guid.Empty) if (entry.Entity.TenantId == Guid.Empty)

View File

@ -9,14 +9,17 @@ namespace line_gestao_api.Data;
public class SeedOptions public class SeedOptions
{ {
public bool Enabled { get; set; } = true; public bool Enabled { get; set; } = true;
public string AdminMasterName { get; set; } = "System Admin"; public string DefaultTenantName { get; set; } = "Default";
public string? AdminMasterEmail { get; set; } = "sysadmin@linegestao.local"; public string AdminName { get; set; } = "Administrador";
public string? AdminMasterPassword { get; set; } = "DevSysAdmin123!"; public string AdminEmail { get; set; } = "admin@linegestao.local";
public string AdminPassword { get; set; } = "DevAdmin123!";
public bool ReapplyAdminCredentialsOnStartup { get; set; } = false; public bool ReapplyAdminCredentialsOnStartup { get; set; } = false;
} }
public static class SeedData public static class SeedData
{ {
public static readonly Guid DefaultTenantId = Guid.Parse("11111111-1111-1111-1111-111111111111");
public static async Task EnsureSeedDataAsync(IServiceProvider services) public static async Task EnsureSeedDataAsync(IServiceProvider services)
{ {
using var scope = services.CreateScope(); using var scope = services.CreateScope();
@ -26,236 +29,224 @@ public static class SeedData
var tenantProvider = scope.ServiceProvider.GetRequiredService<ITenantProvider>(); var tenantProvider = scope.ServiceProvider.GetRequiredService<ITenantProvider>();
var options = scope.ServiceProvider.GetRequiredService<IOptions<SeedOptions>>().Value; var options = scope.ServiceProvider.GetRequiredService<IOptions<SeedOptions>>().Value;
if (db.Database.IsRelational()) await db.Database.MigrateAsync();
{
await db.Database.MigrateAsync();
}
else
{
await db.Database.EnsureCreatedAsync();
}
if (!options.Enabled) if (!options.Enabled)
{ {
return; return;
} }
var systemTenantId = SystemTenantConstants.SystemTenantId; var roles = new[] { "admin", "gestor", "operador", "leitura" };
var roles = AppRoles.All;
foreach (var role in roles) foreach (var role in roles)
{ {
if (!await roleManager.RoleExistsAsync(role)) if (!await roleManager.RoleExistsAsync(role))
{ {
var roleResult = await roleManager.CreateAsync(new IdentityRole<Guid>(role)); await roleManager.CreateAsync(new IdentityRole<Guid>(role));
EnsureIdentitySucceeded(roleResult, $"Falha ao criar role '{role}'.");
} }
} }
await MigrateLegacyRolesAsync(db, roleManager); var tenant = await db.Tenants.FirstOrDefaultAsync(t => t.Id == DefaultTenantId);
if (tenant == null)
var systemTenant = await db.Tenants.FirstOrDefaultAsync(t => t.Id == systemTenantId);
if (systemTenant == null)
{ {
systemTenant = new Tenant tenant = new Tenant
{ {
Id = systemTenantId, Id = DefaultTenantId,
NomeOficial = SystemTenantConstants.SystemTenantNomeOficial, Name = options.DefaultTenantName,
IsSystem = true,
Ativo = true,
CreatedAt = DateTime.UtcNow CreatedAt = DateTime.UtcNow
}; };
db.Tenants.Add(systemTenant);
} db.Tenants.Add(tenant);
else await db.SaveChangesAsync();
{
systemTenant.NomeOficial = SystemTenantConstants.SystemTenantNomeOficial;
systemTenant.IsSystem = true;
systemTenant.Ativo = true;
} }
await db.SaveChangesAsync(); await NormalizeLegacyTenantDataAsync(db, tenant.Id);
var emailFromEnv = Environment.GetEnvironmentVariable("SYSADMIN_EMAIL") tenantProvider.SetTenantId(tenant.Id);
?? Environment.GetEnvironmentVariable("ADMIN_MASTER_EMAIL");
var passwordFromEnv = Environment.GetEnvironmentVariable("SYSADMIN_PASSWORD")
?? Environment.GetEnvironmentVariable("ADMIN_MASTER_PASSWORD");
var adminMasterEmail = (emailFromEnv ?? options.AdminMasterEmail ?? string.Empty).Trim().ToLowerInvariant(); var normalizedEmail = userManager.NormalizeEmail(options.AdminEmail);
var adminMasterPassword = passwordFromEnv ?? options.AdminMasterPassword ?? string.Empty; var existingAdmin = await userManager.Users
.FirstOrDefaultAsync(u => u.NormalizedEmail == normalizedEmail && u.TenantId == tenant.Id);
if (string.IsNullOrWhiteSpace(adminMasterEmail) || string.IsNullOrWhiteSpace(adminMasterPassword)) if (existingAdmin == null)
{ {
throw new InvalidOperationException( var adminUser = new ApplicationUser
"Credenciais do sysadmin ausentes. Defina SYSADMIN_EMAIL e SYSADMIN_PASSWORD (ou Seed:AdminMasterEmail/Seed:AdminMasterPassword).");
}
var normalizedEmail = userManager.NormalizeEmail(adminMasterEmail);
var previousTenant = tenantProvider.TenantId;
tenantProvider.SetTenantId(systemTenantId);
try
{
var existingAdminMaster = await userManager.Users
.IgnoreQueryFilters()
.FirstOrDefaultAsync(u => u.TenantId == systemTenantId && u.NormalizedEmail == normalizedEmail);
if (existingAdminMaster == null)
{ {
var adminMaster = new ApplicationUser UserName = options.AdminEmail,
{ Email = options.AdminEmail,
Name = options.AdminMasterName, Name = options.AdminName,
Email = adminMasterEmail, TenantId = tenant.Id,
UserName = adminMasterEmail, EmailConfirmed = true,
TenantId = systemTenantId, IsActive = true,
EmailConfirmed = true, LockoutEnabled = true
IsActive = true, };
LockoutEnabled = true
};
var createResult = await userManager.CreateAsync(adminMaster, adminMasterPassword); var createResult = await userManager.CreateAsync(adminUser, options.AdminPassword);
EnsureIdentitySucceeded(createResult, "Falha ao criar usuário sysadmin."); if (createResult.Succeeded)
{
var addRoleResult = await userManager.AddToRoleAsync(adminMaster, SystemTenantConstants.SystemRole); await userManager.AddToRoleAsync(adminUser, "admin");
EnsureIdentitySucceeded(addRoleResult, "Falha ao associar role sysadmin ao usuário inicial.");
} }
else }
else if (options.ReapplyAdminCredentialsOnStartup)
{
existingAdmin.Name = options.AdminName;
existingAdmin.Email = options.AdminEmail;
existingAdmin.UserName = options.AdminEmail;
existingAdmin.EmailConfirmed = true;
existingAdmin.IsActive = true;
existingAdmin.LockoutEnabled = true;
await userManager.SetLockoutEndDateAsync(existingAdmin, null);
await userManager.ResetAccessFailedCountAsync(existingAdmin);
await userManager.UpdateAsync(existingAdmin);
var resetToken = await userManager.GeneratePasswordResetTokenAsync(existingAdmin);
var resetPasswordResult = await userManager.ResetPasswordAsync(existingAdmin, resetToken, options.AdminPassword);
if (!resetPasswordResult.Succeeded)
{ {
existingAdminMaster.Name = options.AdminMasterName; var removePasswordResult = await userManager.RemovePasswordAsync(existingAdmin);
existingAdminMaster.Email = adminMasterEmail; if (removePasswordResult.Succeeded)
existingAdminMaster.UserName = adminMasterEmail;
existingAdminMaster.EmailConfirmed = true;
existingAdminMaster.IsActive = true;
existingAdminMaster.LockoutEnabled = true;
var updateResult = await userManager.UpdateAsync(existingAdminMaster);
EnsureIdentitySucceeded(updateResult, "Falha ao atualizar usuário sysadmin.");
if (options.ReapplyAdminCredentialsOnStartup)
{ {
await userManager.SetLockoutEndDateAsync(existingAdminMaster, null); await userManager.AddPasswordAsync(existingAdmin, options.AdminPassword);
await userManager.ResetAccessFailedCountAsync(existingAdminMaster);
var resetToken = await userManager.GeneratePasswordResetTokenAsync(existingAdminMaster);
var resetPasswordResult = await userManager.ResetPasswordAsync(existingAdminMaster, resetToken, adminMasterPassword);
if (!resetPasswordResult.Succeeded)
{
var removePasswordResult = await userManager.RemovePasswordAsync(existingAdminMaster);
if (removePasswordResult.Succeeded)
{
var addPasswordResult = await userManager.AddPasswordAsync(existingAdminMaster, adminMasterPassword);
EnsureIdentitySucceeded(addPasswordResult, "Falha ao definir senha do sysadmin.");
}
else
{
var addPasswordResult = await userManager.AddPasswordAsync(existingAdminMaster, adminMasterPassword);
EnsureIdentitySucceeded(addPasswordResult, "Falha ao definir senha do sysadmin.");
}
}
}
if (!await userManager.IsInRoleAsync(existingAdminMaster, SystemTenantConstants.SystemRole))
{
var addRoleResult = await userManager.AddToRoleAsync(existingAdminMaster, SystemTenantConstants.SystemRole);
EnsureIdentitySucceeded(addRoleResult, "Falha ao associar role sysadmin ao usuário inicial.");
} }
} }
}
finally
{
tenantProvider.SetTenantId(previousTenant);
}
}
private static void EnsureIdentitySucceeded(IdentityResult result, string message) if (!await userManager.IsInRoleAsync(existingAdmin, "admin"))
{
if (result.Succeeded)
{
return;
}
var details = string.Join("; ", result.Errors.Select(e => e.Description));
throw new InvalidOperationException($"{message} Detalhes: {details}");
}
private static async Task MigrateLegacyRolesAsync(AppDbContext db, RoleManager<IdentityRole<Guid>> roleManager)
{
await MigrateLegacyRoleAsync(db, roleManager, "admin_master", AppRoles.SysAdmin);
await MigrateLegacyRoleAsync(db, roleManager, "admin", AppRoles.SysAdmin);
await MigrateLegacyRoleAsync(db, roleManager, "leitura", AppRoles.Cliente);
await MigrateLegacyRoleAsync(db, roleManager, "operador", AppRoles.Cliente);
}
private static async Task MigrateLegacyRoleAsync(
AppDbContext db,
RoleManager<IdentityRole<Guid>> roleManager,
string legacyRole,
string newRole)
{
var legacyRoleId = await roleManager.Roles
.Where(r => r.Name == legacyRole)
.Select(r => (Guid?)r.Id)
.FirstOrDefaultAsync();
if (!legacyRoleId.HasValue)
{
return;
}
var newRoleId = await roleManager.Roles
.Where(r => r.Name == newRole)
.Select(r => (Guid?)r.Id)
.FirstOrDefaultAsync();
if (!newRoleId.HasValue)
{
return;
}
var legacyUserIds = await db.UserRoles
.Where(ur => ur.RoleId == legacyRoleId.Value)
.Select(ur => ur.UserId)
.Distinct()
.ToListAsync();
if (legacyUserIds.Count == 0)
{
return;
}
var alreadyInNewRole = await db.UserRoles
.Where(ur => ur.RoleId == newRoleId.Value && legacyUserIds.Contains(ur.UserId))
.Select(ur => ur.UserId)
.ToListAsync();
var existingSet = alreadyInNewRole.ToHashSet();
foreach (var userId in legacyUserIds)
{
if (!existingSet.Contains(userId))
{ {
db.UserRoles.Add(new IdentityUserRole<Guid> await userManager.AddToRoleAsync(existingAdmin, "admin");
{
UserId = userId,
RoleId = newRoleId.Value
});
} }
} }
var legacyAssignments = await db.UserRoles tenantProvider.SetTenantId(null);
.Where(ur => ur.RoleId == legacyRoleId.Value) }
.ToListAsync();
if (legacyAssignments.Count > 0)
{
db.UserRoles.RemoveRange(legacyAssignments);
}
await db.SaveChangesAsync(); private static async Task NormalizeLegacyTenantDataAsync(AppDbContext db, Guid defaultTenantId)
{
if (defaultTenantId == Guid.Empty)
return;
var legacyRoleStillUsed = await db.UserRoles.AnyAsync(ur => ur.RoleId == legacyRoleId.Value); await db.Users
if (!legacyRoleStillUsed) .IgnoreQueryFilters()
{ .Where(x => x.TenantId == Guid.Empty)
var legacyRoleEntity = await roleManager.Roles.FirstOrDefaultAsync(r => r.Id == legacyRoleId.Value); .ExecuteUpdateAsync(setters => setters.SetProperty(x => x.TenantId, defaultTenantId));
if (legacyRoleEntity != null)
{ await db.MobileLines
await roleManager.DeleteAsync(legacyRoleEntity); .IgnoreQueryFilters()
} .Where(x => x.TenantId == Guid.Empty)
} .ExecuteUpdateAsync(setters => setters.SetProperty(x => x.TenantId, defaultTenantId));
await db.MuregLines
.IgnoreQueryFilters()
.Where(x => x.TenantId == Guid.Empty)
.ExecuteUpdateAsync(setters => setters.SetProperty(x => x.TenantId, defaultTenantId));
await db.BillingClients
.IgnoreQueryFilters()
.Where(x => x.TenantId == Guid.Empty)
.ExecuteUpdateAsync(setters => setters.SetProperty(x => x.TenantId, defaultTenantId));
await db.UserDatas
.IgnoreQueryFilters()
.Where(x => x.TenantId == Guid.Empty)
.ExecuteUpdateAsync(setters => setters.SetProperty(x => x.TenantId, defaultTenantId));
await db.VigenciaLines
.IgnoreQueryFilters()
.Where(x => x.TenantId == Guid.Empty)
.ExecuteUpdateAsync(setters => setters.SetProperty(x => x.TenantId, defaultTenantId));
await db.TrocaNumeroLines
.IgnoreQueryFilters()
.Where(x => x.TenantId == Guid.Empty)
.ExecuteUpdateAsync(setters => setters.SetProperty(x => x.TenantId, defaultTenantId));
await db.ChipVirgemLines
.IgnoreQueryFilters()
.Where(x => x.TenantId == Guid.Empty)
.ExecuteUpdateAsync(setters => setters.SetProperty(x => x.TenantId, defaultTenantId));
await db.ControleRecebidoLines
.IgnoreQueryFilters()
.Where(x => x.TenantId == Guid.Empty)
.ExecuteUpdateAsync(setters => setters.SetProperty(x => x.TenantId, defaultTenantId));
await db.Notifications
.IgnoreQueryFilters()
.Where(x => x.TenantId == Guid.Empty)
.ExecuteUpdateAsync(setters => setters.SetProperty(x => x.TenantId, defaultTenantId));
await db.ResumoMacrophonyPlans
.IgnoreQueryFilters()
.Where(x => x.TenantId == Guid.Empty)
.ExecuteUpdateAsync(setters => setters.SetProperty(x => x.TenantId, defaultTenantId));
await db.ResumoMacrophonyTotals
.IgnoreQueryFilters()
.Where(x => x.TenantId == Guid.Empty)
.ExecuteUpdateAsync(setters => setters.SetProperty(x => x.TenantId, defaultTenantId));
await db.ResumoVivoLineResumos
.IgnoreQueryFilters()
.Where(x => x.TenantId == Guid.Empty)
.ExecuteUpdateAsync(setters => setters.SetProperty(x => x.TenantId, defaultTenantId));
await db.ResumoVivoLineTotals
.IgnoreQueryFilters()
.Where(x => x.TenantId == Guid.Empty)
.ExecuteUpdateAsync(setters => setters.SetProperty(x => x.TenantId, defaultTenantId));
await db.ResumoClienteEspeciais
.IgnoreQueryFilters()
.Where(x => x.TenantId == Guid.Empty)
.ExecuteUpdateAsync(setters => setters.SetProperty(x => x.TenantId, defaultTenantId));
await db.ResumoPlanoContratoResumos
.IgnoreQueryFilters()
.Where(x => x.TenantId == Guid.Empty)
.ExecuteUpdateAsync(setters => setters.SetProperty(x => x.TenantId, defaultTenantId));
await db.ResumoPlanoContratoTotals
.IgnoreQueryFilters()
.Where(x => x.TenantId == Guid.Empty)
.ExecuteUpdateAsync(setters => setters.SetProperty(x => x.TenantId, defaultTenantId));
await db.ResumoLineTotais
.IgnoreQueryFilters()
.Where(x => x.TenantId == Guid.Empty)
.ExecuteUpdateAsync(setters => setters.SetProperty(x => x.TenantId, defaultTenantId));
await db.ResumoReservaLines
.IgnoreQueryFilters()
.Where(x => x.TenantId == Guid.Empty)
.ExecuteUpdateAsync(setters => setters.SetProperty(x => x.TenantId, defaultTenantId));
await db.ResumoReservaTotals
.IgnoreQueryFilters()
.Where(x => x.TenantId == Guid.Empty)
.ExecuteUpdateAsync(setters => setters.SetProperty(x => x.TenantId, defaultTenantId));
await db.ParcelamentoLines
.IgnoreQueryFilters()
.Where(x => x.TenantId == Guid.Empty)
.ExecuteUpdateAsync(setters => setters.SetProperty(x => x.TenantId, defaultTenantId));
await db.ParcelamentoMonthValues
.IgnoreQueryFilters()
.Where(x => x.TenantId == Guid.Empty)
.ExecuteUpdateAsync(setters => setters.SetProperty(x => x.TenantId, defaultTenantId));
await db.AuditLogs
.IgnoreQueryFilters()
.Where(x => x.TenantId == Guid.Empty)
.ExecuteUpdateAsync(setters => setters.SetProperty(x => x.TenantId, defaultTenantId));
await db.ImportAuditRuns
.IgnoreQueryFilters()
.Where(x => x.TenantId == Guid.Empty)
.ExecuteUpdateAsync(setters => setters.SetProperty(x => x.TenantId, defaultTenantId));
await db.ImportAuditIssues
.IgnoreQueryFilters()
.Where(x => x.TenantId == Guid.Empty)
.ExecuteUpdateAsync(setters => setters.SetProperty(x => x.TenantId, defaultTenantId));
} }
} }

View File

@ -1,24 +0,0 @@
using System;
using System.Collections.Generic;
namespace line_gestao_api.Dtos
{
public class CreateMobileLinesBatchRequestDto
{
public List<CreateMobileLineDto> Lines { get; set; } = new();
}
public class CreateMobileLinesBatchResultDto
{
public int Created { get; set; }
public List<CreateMobileLinesBatchCreatedItemDto> Items { get; set; } = new();
}
public class CreateMobileLinesBatchCreatedItemDto
{
public Guid Id { get; set; }
public int Item { get; set; }
public string? Linha { get; set; }
public string? Cliente { get; set; }
}
}

View File

@ -1,74 +0,0 @@
using System;
using System.Collections.Generic;
namespace line_gestao_api.Dtos
{
public sealed class LinesBatchExcelPreviewResultDto
{
public string? FileName { get; set; }
public string? SheetName { get; set; }
public int NextItemStart { get; set; }
public int TotalRows { get; set; }
public int ValidRows { get; set; }
public int InvalidRows { get; set; }
public int DuplicateRows { get; set; }
public bool CanProceed { get; set; }
public List<LinesBatchExcelIssueDto> HeaderErrors { get; set; } = new();
public List<LinesBatchExcelIssueDto> HeaderWarnings { get; set; } = new();
public List<LinesBatchExcelPreviewRowDto> Rows { get; set; } = new();
}
public sealed class LinesBatchExcelPreviewRowDto
{
public int SourceRowNumber { get; set; }
public int? SourceItem { get; set; }
public int? GeneratedItemPreview { get; set; }
public bool Valid { get; set; }
public bool DuplicateLinhaInFile { get; set; }
public bool DuplicateChipInFile { get; set; }
public bool DuplicateLinhaInSystem { get; set; }
public bool DuplicateChipInSystem { get; set; }
public CreateMobileLineDto Data { get; set; } = new();
public List<LinesBatchExcelIssueDto> Errors { get; set; } = new();
public List<LinesBatchExcelIssueDto> Warnings { get; set; } = new();
}
public sealed class LinesBatchExcelIssueDto
{
public string? Column { get; set; }
public string Message { get; set; } = string.Empty;
}
public sealed class AssignReservaLinesRequestDto
{
public string? ClienteDestino { get; set; }
public string? UsuarioDestino { get; set; }
public string? SkilDestino { get; set; }
public List<Guid> LineIds { get; set; } = new();
}
public sealed class MoveLinesToReservaRequestDto
{
public List<Guid> LineIds { get; set; } = new();
}
public sealed class AssignReservaLinesResultDto
{
public int Requested { get; set; }
public int Updated { get; set; }
public int Failed { get; set; }
public List<AssignReservaLineItemResultDto> Items { get; set; } = new();
}
public sealed class AssignReservaLineItemResultDto
{
public Guid Id { get; set; }
public int Item { get; set; }
public string? Linha { get; set; }
public string? Chip { get; set; }
public string? ClienteAnterior { get; set; }
public string? ClienteNovo { get; set; }
public bool Success { get; set; }
public string Message { get; set; } = string.Empty;
}
}

View File

@ -14,7 +14,6 @@
public string? Skil { get; set; } public string? Skil { get; set; }
public string? Modalidade { get; set; } public string? Modalidade { get; set; }
public string? VencConta { get; set; } public string? VencConta { get; set; }
public decimal? FranquiaLine { get; set; }
// Campos para filtro deterministico de adicionais no frontend // Campos para filtro deterministico de adicionais no frontend
public decimal? GestaoVozDados { get; set; } public decimal? GestaoVozDados { get; set; }

View File

@ -1,24 +0,0 @@
namespace line_gestao_api.Dtos;
public class SystemTenantListItemDto
{
public Guid TenantId { get; set; }
public string NomeOficial { get; set; } = string.Empty;
}
public class CreateSystemTenantUserRequest
{
public string Name { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
public string Password { get; set; } = string.Empty;
public List<string> Roles { get; set; } = new();
public bool ClientCredentialsOnly { get; set; }
}
public class SystemTenantUserCreatedDto
{
public Guid UserId { get; set; }
public Guid TenantId { get; set; }
public string Email { get; set; } = string.Empty;
public IReadOnlyList<string> Roles { get; set; } = Array.Empty<string>();
}

View File

@ -14,10 +14,6 @@ namespace line_gestao_api.Dtos
public string? PlanoContrato { get; set; } public string? PlanoContrato { get; set; }
public DateTime? DtEfetivacaoServico { get; set; } public DateTime? DtEfetivacaoServico { get; set; }
public DateTime? DtTerminoFidelizacao { get; set; } public DateTime? DtTerminoFidelizacao { get; set; }
public int? AutoRenewYears { get; set; }
public DateTime? AutoRenewReferenceEndDate { get; set; }
public DateTime? AutoRenewConfiguredAt { get; set; }
public DateTime? LastAutoRenewedAt { get; set; }
public decimal? Total { get; set; } public decimal? Total { get; set; }
} }
@ -53,11 +49,6 @@ namespace line_gestao_api.Dtos
public decimal? Total { get; set; } public decimal? Total { get; set; }
} }
public class ConfigureVigenciaRenewalRequest
{
public int Years { get; set; }
}
public class VigenciaClientGroupDto public class VigenciaClientGroupDto
{ {
public string Cliente { get; set; } = ""; public string Cliente { get; set; } = "";

View File

@ -1,110 +0,0 @@
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using line_gestao_api.Data;
#nullable disable
namespace line_gestao_api.Migrations
{
[DbContext(typeof(AppDbContext))]
[Migration("20260226130000_CreateTenantsAndAuditLogsSystemContracts")]
public partial class CreateTenantsAndAuditLogsSystemContracts : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql("""
DO $$
BEGIN
IF EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_name = 'Tenants'
AND column_name = 'Name'
) AND NOT EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_name = 'Tenants'
AND column_name = 'NomeOficial'
) THEN
ALTER TABLE "Tenants" RENAME COLUMN "Name" TO "NomeOficial";
END IF;
END
$$;
""");
migrationBuilder.Sql("""ALTER TABLE "Tenants" ADD COLUMN IF NOT EXISTS "IsSystem" boolean NOT NULL DEFAULT FALSE;""");
migrationBuilder.Sql("""ALTER TABLE "Tenants" ADD COLUMN IF NOT EXISTS "Ativo" boolean NOT NULL DEFAULT TRUE;""");
migrationBuilder.Sql("""ALTER TABLE "Tenants" ADD COLUMN IF NOT EXISTS "SourceType" character varying(80) NULL;""");
migrationBuilder.Sql("""ALTER TABLE "Tenants" ADD COLUMN IF NOT EXISTS "SourceKey" character varying(300) NULL;""");
migrationBuilder.Sql("""
UPDATE "Tenants"
SET "NomeOficial" = COALESCE(NULLIF("NomeOficial", ''), 'TENANT_SEM_NOME')
WHERE "NomeOficial" IS NULL OR "NomeOficial" = '';
""");
migrationBuilder.Sql("""ALTER TABLE "Tenants" ALTER COLUMN "NomeOficial" SET NOT NULL;""");
migrationBuilder.Sql("""CREATE UNIQUE INDEX IF NOT EXISTS "IX_Tenants_SourceType_SourceKey" ON "Tenants" ("SourceType", "SourceKey");""");
migrationBuilder.Sql("""CREATE INDEX IF NOT EXISTS "IX_Tenants_IsSystem_Ativo" ON "Tenants" ("IsSystem", "Ativo");""");
migrationBuilder.Sql("""ALTER TABLE "AuditLogs" ADD COLUMN IF NOT EXISTS "ActorUserId" uuid NULL;""");
migrationBuilder.Sql("""ALTER TABLE "AuditLogs" ADD COLUMN IF NOT EXISTS "ActorTenantId" uuid NOT NULL DEFAULT '00000000-0000-0000-0000-000000000000';""");
migrationBuilder.Sql("""ALTER TABLE "AuditLogs" ADD COLUMN IF NOT EXISTS "TargetTenantId" uuid NOT NULL DEFAULT '00000000-0000-0000-0000-000000000000';""");
migrationBuilder.Sql("""ALTER TABLE "AuditLogs" ADD COLUMN IF NOT EXISTS "MetadataJson" jsonb NOT NULL DEFAULT '{}'::jsonb;""");
migrationBuilder.Sql("""
UPDATE "AuditLogs"
SET "ActorUserId" = COALESCE("ActorUserId", "UserId"),
"ActorTenantId" = CASE
WHEN "ActorTenantId" = '00000000-0000-0000-0000-000000000000' THEN "TenantId"
ELSE "ActorTenantId"
END,
"TargetTenantId" = CASE
WHEN "TargetTenantId" = '00000000-0000-0000-0000-000000000000' THEN "TenantId"
ELSE "TargetTenantId"
END,
"MetadataJson" = COALESCE("MetadataJson", '{}'::jsonb);
""");
migrationBuilder.Sql("""CREATE INDEX IF NOT EXISTS "IX_AuditLogs_ActorTenantId" ON "AuditLogs" ("ActorTenantId");""");
migrationBuilder.Sql("""CREATE INDEX IF NOT EXISTS "IX_AuditLogs_TargetTenantId" ON "AuditLogs" ("TargetTenantId");""");
migrationBuilder.Sql("""CREATE INDEX IF NOT EXISTS "IX_AuditLogs_ActorUserId" ON "AuditLogs" ("ActorUserId");""");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql("""DROP INDEX IF EXISTS "IX_AuditLogs_ActorUserId";""");
migrationBuilder.Sql("""DROP INDEX IF EXISTS "IX_AuditLogs_TargetTenantId";""");
migrationBuilder.Sql("""DROP INDEX IF EXISTS "IX_AuditLogs_ActorTenantId";""");
migrationBuilder.Sql("""ALTER TABLE "AuditLogs" DROP COLUMN IF EXISTS "MetadataJson";""");
migrationBuilder.Sql("""ALTER TABLE "AuditLogs" DROP COLUMN IF EXISTS "TargetTenantId";""");
migrationBuilder.Sql("""ALTER TABLE "AuditLogs" DROP COLUMN IF EXISTS "ActorTenantId";""");
migrationBuilder.Sql("""ALTER TABLE "AuditLogs" DROP COLUMN IF EXISTS "ActorUserId";""");
migrationBuilder.Sql("""DROP INDEX IF EXISTS "IX_Tenants_IsSystem_Ativo";""");
migrationBuilder.Sql("""DROP INDEX IF EXISTS "IX_Tenants_SourceType_SourceKey";""");
migrationBuilder.Sql("""ALTER TABLE "Tenants" DROP COLUMN IF EXISTS "SourceKey";""");
migrationBuilder.Sql("""ALTER TABLE "Tenants" DROP COLUMN IF EXISTS "SourceType";""");
migrationBuilder.Sql("""ALTER TABLE "Tenants" DROP COLUMN IF EXISTS "Ativo";""");
migrationBuilder.Sql("""ALTER TABLE "Tenants" DROP COLUMN IF EXISTS "IsSystem";""");
migrationBuilder.Sql("""
DO $$
BEGIN
IF EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_name = 'Tenants'
AND column_name = 'NomeOficial'
) AND NOT EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_name = 'Tenants'
AND column_name = 'Name'
) THEN
ALTER TABLE "Tenants" RENAME COLUMN "NomeOficial" TO "Name";
END IF;
END
$$;
""");
}
}
}

View File

@ -1,36 +0,0 @@
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using line_gestao_api.Data;
#nullable disable
namespace line_gestao_api.Migrations
{
[DbContext(typeof(AppDbContext))]
[Migration("20260226130100_AddTenantIdToMobileLinesIfNeeded")]
public partial class AddTenantIdToMobileLinesIfNeeded : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql("""
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_name = 'MobileLines'
AND column_name = 'TenantId'
) THEN
ALTER TABLE "MobileLines" ADD COLUMN "TenantId" uuid NULL;
END IF;
END
$$;
""");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
// No-op intencional para evitar perda de dados em bancos legados.
}
}
}

View File

@ -1,74 +0,0 @@
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using line_gestao_api.Data;
#nullable disable
namespace line_gestao_api.Migrations
{
[DbContext(typeof(AppDbContext))]
[Migration("20260226130200_BackfillTenantsFromDistinctMobileLinesCliente")]
public partial class BackfillTenantsFromDistinctMobileLinesCliente : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql("""
DO $$
BEGIN
IF EXISTS (
SELECT 1
FROM "MobileLines"
WHERE "Cliente" IS NULL OR btrim("Cliente") = ''
) THEN
RAISE EXCEPTION 'Backfill abortado: MobileLines.Cliente possui valores NULL/vazios. Corrija os dados antes de migrar.';
END IF;
END
$$;
""");
migrationBuilder.Sql("""CREATE EXTENSION IF NOT EXISTS pgcrypto;""");
migrationBuilder.Sql("""
INSERT INTO "Tenants" (
"Id",
"NomeOficial",
"IsSystem",
"Ativo",
"SourceType",
"SourceKey",
"CreatedAt"
)
SELECT
gen_random_uuid(),
src."Cliente",
FALSE,
TRUE,
'MobileLines.Cliente',
src."Cliente",
NOW()
FROM (
SELECT DISTINCT "Cliente"
FROM "MobileLines"
) src
LEFT JOIN "Tenants" t
ON t."SourceType" = 'MobileLines.Cliente'
AND t."SourceKey" = src."Cliente"
WHERE t."Id" IS NULL;
""");
migrationBuilder.Sql("""
UPDATE "Tenants"
SET "NomeOficial" = "SourceKey",
"IsSystem" = FALSE,
"Ativo" = TRUE
WHERE "SourceType" = 'MobileLines.Cliente'
AND "SourceKey" IS NOT NULL;
""");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
// No-op intencional. Evita remover tenants já em uso.
}
}
}

View File

@ -1,47 +0,0 @@
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using line_gestao_api.Data;
#nullable disable
namespace line_gestao_api.Migrations
{
[DbContext(typeof(AppDbContext))]
[Migration("20260226130300_BackfillMobileLinesTenantIdFromTenantSourceKey")]
public partial class BackfillMobileLinesTenantIdFromTenantSourceKey : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql("""
UPDATE "MobileLines" m
SET "TenantId" = t."Id"
FROM "Tenants" t
WHERE t."SourceType" = 'MobileLines.Cliente'
AND t."SourceKey" = m."Cliente"
AND (m."TenantId" IS NULL OR m."TenantId" <> t."Id");
""");
migrationBuilder.Sql("""
DO $$
BEGIN
IF EXISTS (
SELECT 1
FROM "MobileLines" m
LEFT JOIN "Tenants" t
ON t."SourceType" = 'MobileLines.Cliente'
AND t."SourceKey" = m."Cliente"
WHERE t."Id" IS NULL
) THEN
RAISE EXCEPTION 'Backfill abortado: existem MobileLines sem tenant correspondente por SourceKey exato.';
END IF;
END
$$;
""");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
// No-op intencional.
}
}
}

View File

@ -1,27 +0,0 @@
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using line_gestao_api.Data;
#nullable disable
namespace line_gestao_api.Migrations
{
[DbContext(typeof(AppDbContext))]
[Migration("20260226130400_MakeMobileLinesTenantIdNotNullAndIndexes")]
public partial class MakeMobileLinesTenantIdNotNullAndIndexes : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql("""ALTER TABLE "MobileLines" ALTER COLUMN "TenantId" DROP DEFAULT;""");
migrationBuilder.Sql("""ALTER TABLE "MobileLines" ALTER COLUMN "TenantId" SET NOT NULL;""");
migrationBuilder.Sql("""CREATE INDEX IF NOT EXISTS "IX_MobileLines_TenantId" ON "MobileLines" ("TenantId");""");
migrationBuilder.Sql("""CREATE UNIQUE INDEX IF NOT EXISTS "IX_MobileLines_TenantId_Linha" ON "MobileLines" ("TenantId", "Linha");""");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql("""DROP INDEX IF EXISTS "IX_MobileLines_TenantId";""");
migrationBuilder.Sql("""ALTER TABLE "MobileLines" ALTER COLUMN "TenantId" DROP NOT NULL;""");
}
}
}

View File

@ -1,68 +0,0 @@
using line_gestao_api.Data;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace line_gestao_api.Migrations
{
[DbContext(typeof(AppDbContext))]
[Migration("20260227120000_AddVigenciaAutoRenewal")]
public partial class AddVigenciaAutoRenewal : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<DateTime>(
name: "AutoRenewConfiguredAt",
table: "VigenciaLines",
type: "timestamp with time zone",
nullable: true);
migrationBuilder.AddColumn<DateTime>(
name: "AutoRenewReferenceEndDate",
table: "VigenciaLines",
type: "timestamp with time zone",
nullable: true);
migrationBuilder.AddColumn<int>(
name: "AutoRenewYears",
table: "VigenciaLines",
type: "integer",
nullable: true);
migrationBuilder.AddColumn<DateTime>(
name: "LastAutoRenewedAt",
table: "VigenciaLines",
type: "timestamp with time zone",
nullable: true);
migrationBuilder.CreateIndex(
name: "IX_VigenciaLines_AutoRenewReferenceEndDate",
table: "VigenciaLines",
column: "AutoRenewReferenceEndDate");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_VigenciaLines_AutoRenewReferenceEndDate",
table: "VigenciaLines");
migrationBuilder.DropColumn(
name: "AutoRenewConfiguredAt",
table: "VigenciaLines");
migrationBuilder.DropColumn(
name: "AutoRenewReferenceEndDate",
table: "VigenciaLines");
migrationBuilder.DropColumn(
name: "AutoRenewYears",
table: "VigenciaLines");
migrationBuilder.DropColumn(
name: "LastAutoRenewedAt",
table: "VigenciaLines");
}
}
}

View File

@ -245,12 +245,6 @@ namespace line_gestao_api.Migrations
.HasMaxLength(20) .HasMaxLength(20)
.HasColumnType("character varying(20)"); .HasColumnType("character varying(20)");
b.Property<Guid?>("ActorUserId")
.HasColumnType("uuid");
b.Property<Guid>("ActorTenantId")
.HasColumnType("uuid");
b.Property<string>("ChangesJson") b.Property<string>("ChangesJson")
.IsRequired() .IsRequired()
.HasColumnType("jsonb"); .HasColumnType("jsonb");
@ -280,10 +274,6 @@ namespace line_gestao_api.Migrations
.HasMaxLength(80) .HasMaxLength(80)
.HasColumnType("character varying(80)"); .HasColumnType("character varying(80)");
b.Property<string>("MetadataJson")
.IsRequired()
.HasColumnType("jsonb");
b.Property<string>("RequestMethod") b.Property<string>("RequestMethod")
.HasMaxLength(10) .HasMaxLength(10)
.HasColumnType("character varying(10)"); .HasColumnType("character varying(10)");
@ -295,9 +285,6 @@ namespace line_gestao_api.Migrations
b.Property<Guid>("TenantId") b.Property<Guid>("TenantId")
.HasColumnType("uuid"); .HasColumnType("uuid");
b.Property<Guid>("TargetTenantId")
.HasColumnType("uuid");
b.Property<string>("UserEmail") b.Property<string>("UserEmail")
.HasMaxLength(200) .HasMaxLength(200)
.HasColumnType("character varying(200)"); .HasColumnType("character varying(200)");
@ -311,10 +298,6 @@ namespace line_gestao_api.Migrations
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("ActorUserId");
b.HasIndex("ActorTenantId");
b.HasIndex("EntityName"); b.HasIndex("EntityName");
b.HasIndex("OccurredAtUtc"); b.HasIndex("OccurredAtUtc");
@ -323,8 +306,6 @@ namespace line_gestao_api.Migrations
b.HasIndex("TenantId"); b.HasIndex("TenantId");
b.HasIndex("TargetTenantId");
b.HasIndex("UserId"); b.HasIndex("UserId");
b.ToTable("AuditLogs"); b.ToTable("AuditLogs");
@ -1376,35 +1357,15 @@ namespace line_gestao_api.Migrations
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
.HasColumnType("uuid"); .HasColumnType("uuid");
b.Property<bool>("Ativo")
.HasColumnType("boolean");
b.Property<DateTime>("CreatedAt") b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone"); .HasColumnType("timestamp with time zone");
b.Property<bool>("IsSystem") b.Property<string>("Name")
.HasColumnType("boolean");
b.Property<string>("NomeOficial")
.IsRequired() .IsRequired()
.HasMaxLength(200) .HasColumnType("text");
.HasColumnType("character varying(200)");
b.Property<string>("SourceKey")
.HasMaxLength(300)
.HasColumnType("character varying(300)");
b.Property<string>("SourceType")
.HasMaxLength(80)
.HasColumnType("character varying(80)");
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("IsSystem", "Ativo");
b.HasIndex("SourceType", "SourceKey")
.IsUnique();
b.ToTable("Tenants"); b.ToTable("Tenants");
}); });
@ -1549,15 +1510,6 @@ namespace line_gestao_api.Migrations
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
.HasColumnType("uuid"); .HasColumnType("uuid");
b.Property<DateTime?>("AutoRenewConfiguredAt")
.HasColumnType("timestamp with time zone");
b.Property<DateTime?>("AutoRenewReferenceEndDate")
.HasColumnType("timestamp with time zone");
b.Property<int?>("AutoRenewYears")
.HasColumnType("integer");
b.Property<string>("Cliente") b.Property<string>("Cliente")
.HasColumnType("text"); .HasColumnType("text");
@ -1576,9 +1528,6 @@ namespace line_gestao_api.Migrations
b.Property<int>("Item") b.Property<int>("Item")
.HasColumnType("integer"); .HasColumnType("integer");
b.Property<DateTime?>("LastAutoRenewedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Linha") b.Property<string>("Linha")
.HasColumnType("text"); .HasColumnType("text");
@ -1601,8 +1550,6 @@ namespace line_gestao_api.Migrations
b.HasIndex("Cliente"); b.HasIndex("Cliente");
b.HasIndex("AutoRenewReferenceEndDate");
b.HasIndex("DtTerminoFidelizacao"); b.HasIndex("DtTerminoFidelizacao");
b.HasIndex("Item"); b.HasIndex("Item");

View File

@ -6,16 +6,10 @@ public class AuditLog : ITenantEntity
{ {
public Guid Id { get; set; } = Guid.NewGuid(); public Guid Id { get; set; } = Guid.NewGuid();
// Compatibilidade com histórico atual + filtro global.
public Guid TenantId { get; set; } public Guid TenantId { get; set; }
public Guid? ActorUserId { get; set; }
public Guid ActorTenantId { get; set; }
public Guid TargetTenantId { get; set; }
public DateTime OccurredAtUtc { get; set; } = DateTime.UtcNow; public DateTime OccurredAtUtc { get; set; } = DateTime.UtcNow;
// Campos legados usados pela tela de histórico.
public Guid? UserId { get; set; } public Guid? UserId { get; set; }
public string? UserName { get; set; } public string? UserName { get; set; }
public string? UserEmail { get; set; } public string? UserEmail { get; set; }
@ -27,7 +21,6 @@ public class AuditLog : ITenantEntity
public string? EntityLabel { get; set; } public string? EntityLabel { get; set; }
public string ChangesJson { get; set; } = "[]"; public string ChangesJson { get; set; } = "[]";
public string MetadataJson { get; set; } = "{}";
public string? RequestPath { get; set; } public string? RequestPath { get; set; }
public string? RequestMethod { get; set; } public string? RequestMethod { get; set; }

View File

@ -3,10 +3,6 @@ namespace line_gestao_api.Models;
public class Tenant public class Tenant
{ {
public Guid Id { get; set; } = Guid.NewGuid(); public Guid Id { get; set; } = Guid.NewGuid();
public string NomeOficial { get; set; } = string.Empty; public string Name { get; set; } = string.Empty;
public bool IsSystem { get; set; }
public bool Ativo { get; set; } = true;
public string? SourceType { get; set; }
public string? SourceKey { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow; public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
} }

View File

@ -20,10 +20,6 @@ namespace line_gestao_api.Models
public DateTime? DtEfetivacaoServico { get; set; } public DateTime? DtEfetivacaoServico { get; set; }
public DateTime? DtTerminoFidelizacao { get; set; } public DateTime? DtTerminoFidelizacao { get; set; }
public int? AutoRenewYears { get; set; }
public DateTime? AutoRenewReferenceEndDate { get; set; }
public DateTime? AutoRenewConfiguredAt { get; set; }
public DateTime? LastAutoRenewedAt { get; set; }
public decimal? Total { get; set; } public decimal? Total { get; set; }

View File

@ -1,5 +1,4 @@
using System.IO; using System.IO;
using System.Security.Claims;
using System.Text; using System.Text;
using System.Threading.RateLimiting; using System.Threading.RateLimiting;
using line_gestao_api.Data; using line_gestao_api.Data;
@ -92,11 +91,9 @@ builder.Services.AddDbContext<AppDbContext>(options =>
builder.Services.AddHttpContextAccessor(); builder.Services.AddHttpContextAccessor();
builder.Services.AddScoped<ITenantProvider, TenantProvider>(); builder.Services.AddScoped<ITenantProvider, TenantProvider>();
builder.Services.AddScoped<IAuditLogBuilder, AuditLogBuilder>(); builder.Services.AddScoped<IAuditLogBuilder, AuditLogBuilder>();
builder.Services.AddScoped<ISystemAuditService, SystemAuditService>();
builder.Services.AddScoped<IVigenciaNotificationSyncService, VigenciaNotificationSyncService>(); builder.Services.AddScoped<IVigenciaNotificationSyncService, VigenciaNotificationSyncService>();
builder.Services.AddScoped<ParcelamentosImportService>(); builder.Services.AddScoped<ParcelamentosImportService>();
builder.Services.AddScoped<GeralDashboardInsightsService>(); builder.Services.AddScoped<GeralDashboardInsightsService>();
builder.Services.AddScoped<GeralSpreadsheetTemplateService>();
builder.Services.AddScoped<SpreadsheetImportAuditService>(); builder.Services.AddScoped<SpreadsheetImportAuditService>();
builder.Services.AddIdentityCore<ApplicationUser>(options => builder.Services.AddIdentityCore<ApplicationUser>(options =>
@ -146,13 +143,7 @@ builder.Services
}; };
}); });
builder.Services.AddAuthorization(options => builder.Services.AddAuthorization();
{
options.AddPolicy("SystemAdmin", policy =>
{
policy.RequireRole(SystemTenantConstants.SystemRole);
});
});
builder.Services.AddRateLimiter(options => builder.Services.AddRateLimiter(options =>
{ {
options.RejectionStatusCode = StatusCodes.Status429TooManyRequests; options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
@ -202,7 +193,3 @@ app.MapControllers();
app.MapGet("/health", () => Results.Ok(new { status = "ok" })); app.MapGet("/health", () => Results.Ok(new { status = "ok" }));
app.Run(); app.Run();
public partial class Program
{
}

View File

@ -1,10 +0,0 @@
namespace line_gestao_api.Services;
public static class AppRoles
{
public const string SysAdmin = "sysadmin";
public const string Gestor = "gestor";
public const string Cliente = "cliente";
public static readonly string[] All = [SysAdmin, Gestor, Cliente];
}

View File

@ -67,7 +67,7 @@ public class AuditLogBuilder : IAuditLogBuilder
public List<AuditLog> BuildAuditLogs(ChangeTracker changeTracker) public List<AuditLog> BuildAuditLogs(ChangeTracker changeTracker)
{ {
var tenantId = _tenantProvider.ActorTenantId; var tenantId = _tenantProvider.TenantId;
if (tenantId == null) if (tenantId == null)
{ {
return new List<AuditLog>(); return new List<AuditLog>();
@ -88,12 +88,6 @@ public class AuditLogBuilder : IAuditLogBuilder
return new List<AuditLog>(); return new List<AuditLog>();
} }
if (IsSystemRequest(requestPath))
{
// Endpoints system usam auditoria explicita com actor/target.
return new List<AuditLog>();
}
var logs = new List<AuditLog>(); var logs = new List<AuditLog>();
foreach (var entry in changeTracker.Entries()) foreach (var entry in changeTracker.Entries())
@ -115,9 +109,6 @@ public class AuditLogBuilder : IAuditLogBuilder
logs.Add(new AuditLog logs.Add(new AuditLog
{ {
TenantId = tenantId.Value, TenantId = tenantId.Value,
ActorUserId = userInfo.UserId,
ActorTenantId = tenantId.Value,
TargetTenantId = tenantId.Value,
OccurredAtUtc = DateTime.UtcNow, OccurredAtUtc = DateTime.UtcNow,
UserId = userInfo.UserId, UserId = userInfo.UserId,
UserName = userInfo.UserName, UserName = userInfo.UserName,
@ -128,7 +119,6 @@ public class AuditLogBuilder : IAuditLogBuilder
EntityId = BuildEntityId(entry), EntityId = BuildEntityId(entry),
EntityLabel = BuildEntityLabel(entry), EntityLabel = BuildEntityLabel(entry),
ChangesJson = JsonSerializer.Serialize(changes, JsonOptions), ChangesJson = JsonSerializer.Serialize(changes, JsonOptions),
MetadataJson = "{}",
RequestPath = requestPath, RequestPath = requestPath,
RequestMethod = requestMethod, RequestMethod = requestMethod,
IpAddress = ipAddress IpAddress = ipAddress
@ -148,16 +138,6 @@ public class AuditLogBuilder : IAuditLogBuilder
return requestPath.Contains("/import-excel", StringComparison.OrdinalIgnoreCase); return requestPath.Contains("/import-excel", StringComparison.OrdinalIgnoreCase);
} }
private static bool IsSystemRequest(string? requestPath)
{
if (string.IsNullOrWhiteSpace(requestPath))
{
return false;
}
return requestPath.StartsWith("/api/system", StringComparison.OrdinalIgnoreCase);
}
private static string ResolveAction(EntityState state) private static string ResolveAction(EntityState state)
=> state switch => state switch
{ {

View File

@ -1,20 +0,0 @@
using System.Security.Cryptography;
using System.Text;
namespace line_gestao_api.Services;
public static class DeterministicGuid
{
public static Guid FromString(string input)
{
if (string.IsNullOrWhiteSpace(input))
{
throw new ArgumentException("Valor obrigatório para gerar Guid determinístico.", nameof(input));
}
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(input));
Span<byte> bytes = stackalloc byte[16];
hash.AsSpan(0, 16).CopyTo(bytes);
return new Guid(bytes);
}
}

View File

@ -1,180 +0,0 @@
using ClosedXML.Excel;
namespace line_gestao_api.Services
{
public class GeralSpreadsheetTemplateService
{
private const string WorksheetName = "GERAL";
private const int HeaderRow = 1;
private const int FirstDataRow = 2;
private const int LastColumn = 31; // A..AE
private static readonly string[] Headers =
{
"CONTA",
"LINHA",
"CHIP",
"CLIENTE",
"USUÁRIO",
"PLANO CONTRATO",
"FRAQUIA",
"VALOR DO PLANO R$",
"GESTÃO VOZ E DADOS R$",
"SKEELO",
"VIVO NEWS PLUS",
"VIVO TRAVEL MUNDO",
"VIVO SYNC",
"VIVO GESTÃO DISPOSITIVO",
"VALOR CONTRATO VIVO",
"FRANQUIA LINE",
"FRANQUIA GESTÃO",
"LOCAÇÃO AP.",
"VALOR CONTRATO LINE",
"DESCONTO",
"LUCRO",
"STATUS",
"DATA DO BLOQUEIO",
"SKIL",
"MODALIDADE",
"CEDENTE",
"SOLICITANTE",
"DATA DA ENTREGA OPERA.",
"DATA DA ENTREGA CLIENTE",
"VENC. DA CONTA",
"TIPO DE CHIP"
};
private static readonly double[] ColumnWidths =
{
11.00, 12.00, 21.43, 70.14, 58.29, 27.71, 11.00, 18.14,
20.71, 11.00, 14.57, 18.14, 11.00, 19.57, 21.43, 14.57,
16.29, 13.00, 20.71, 13.00, 13.00, 14.57, 16.29, 13.00,
16.29, 39.43, 27.86, 25.00, 27.71, 16.29, 13.00
};
private static readonly int[] TextColumns =
{
1, 2, 3, 4, 5, 6, 7, 16, 17, 22, 24, 25, 26, 27, 31
};
private static readonly int[] CurrencyColumns =
{
8, 9, 10, 11, 12, 13, 14, 15, 18, 19, 20, 21
};
private static readonly int[] DateColumns =
{
23, 28, 29, 30
};
public byte[] BuildPlanilhaGeralTemplate()
{
using var workbook = new XLWorkbook();
var ws = workbook.Worksheets.Add(WorksheetName);
BuildHeader(ws);
ConfigureColumns(ws);
ConfigureDataFormatting(ws);
ConfigureSheetView(ws);
using var stream = new MemoryStream();
workbook.SaveAs(stream);
return stream.ToArray();
}
private static void BuildHeader(IXLWorksheet ws)
{
for (var i = 0; i < Headers.Length; i++)
{
ws.Cell(HeaderRow, i + 1).Value = Headers[i];
}
var headerRange = ws.Range(HeaderRow, 1, HeaderRow, LastColumn);
headerRange.Style.Font.FontName = "Calibri";
headerRange.Style.Font.FontSize = 11;
headerRange.Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Center;
headerRange.Style.Alignment.Vertical = XLAlignmentVerticalValues.Center;
headerRange.Style.Border.TopBorder = XLBorderStyleValues.Thin;
headerRange.Style.Border.BottomBorder = XLBorderStyleValues.Thin;
headerRange.Style.Border.LeftBorder = XLBorderStyleValues.Thin;
headerRange.Style.Border.RightBorder = XLBorderStyleValues.Thin;
ws.Row(HeaderRow).Height = 14.25;
var navy = XLColor.FromHtml("#002060");
var purple = XLColor.FromHtml("#7030A0");
var orange = XLColor.FromHtml("#D9A87E");
var red = XLColor.FromHtml("#FF0000");
var yellow = XLColor.FromHtml("#FFFF00");
var white = XLColor.White;
var black = XLColor.Black;
ApplyHeaderBlock(ws, 1, 6, navy, white, bold: true); // A-F
ApplyHeaderBlock(ws, 22, 31, navy, white, bold: true); // V-AE
ApplyHeaderBlock(ws, 7, 15, purple, white, bold: true); // G-O
ApplyHeaderBlock(ws, 16, 19, orange, white, bold: true);// P-S
ApplyHeaderBlock(ws, 20, 20, red, white, bold: true); // T
ApplyHeaderBlock(ws, 21, 21, yellow, black, bold: true);// U
// Exceções no bloco azul (sem negrito): CHIP, CLIENTE, USUÁRIO => C, D, E
ws.Cell(1, 3).Style.Font.Bold = false;
ws.Cell(1, 4).Style.Font.Bold = false;
ws.Cell(1, 5).Style.Font.Bold = false;
}
private static void ApplyHeaderBlock(
IXLWorksheet ws,
int startCol,
int endCol,
XLColor bgColor,
XLColor fontColor,
bool bold)
{
var range = ws.Range(HeaderRow, startCol, HeaderRow, endCol);
range.Style.Fill.BackgroundColor = bgColor;
range.Style.Font.FontColor = fontColor;
range.Style.Font.Bold = bold;
}
private static void ConfigureColumns(IXLWorksheet ws)
{
for (var i = 0; i < ColumnWidths.Length; i++)
{
ws.Column(i + 1).Width = ColumnWidths[i];
}
}
private static void ConfigureDataFormatting(IXLWorksheet ws)
{
// Prepara um range vazio com estilo base para facilitar preenchimento manual
var dataPreviewRange = ws.Range(FirstDataRow, 1, 1000, LastColumn);
dataPreviewRange.Style.Font.FontName = "Calibri";
dataPreviewRange.Style.Font.FontSize = 11;
dataPreviewRange.Style.Alignment.Vertical = XLAlignmentVerticalValues.Center;
foreach (var col in TextColumns)
{
ws.Column(col).Style.NumberFormat.Format = "@";
}
foreach (var col in CurrencyColumns)
{
ws.Column(col).Style.NumberFormat.Format = "\"R$\" #,##0.00";
}
foreach (var col in DateColumns)
{
ws.Column(col).Style.DateFormat.Format = "dd/MM/yyyy";
}
// O campo ITÉM é gerado internamente pelo sistema e não faz parte do template.
}
private static void ConfigureSheetView(IXLWorksheet ws)
{
ws.ShowGridLines = false;
ws.SheetView.FreezeRows(1); // Freeze em A2 (mantém linha 1 fixa)
ws.Range(1, 1, 1, LastColumn).SetAutoFilter();
}
}
}

View File

@ -1,6 +0,0 @@
namespace line_gestao_api.Services;
public interface ISystemAuditService
{
Task LogAsync(string action, Guid targetTenantId, object? metadata = null, CancellationToken cancellationToken = default);
}

View File

@ -2,8 +2,6 @@ namespace line_gestao_api.Services;
public interface ITenantProvider public interface ITenantProvider
{ {
Guid? ActorTenantId { get; }
Guid? TenantId { get; } Guid? TenantId { get; }
bool HasGlobalViewAccess { get; }
void SetTenantId(Guid? tenantId); void SetTenantId(Guid? tenantId);
} }

View File

@ -1,8 +0,0 @@
namespace line_gestao_api.Services;
public static class SystemAuditActions
{
public const string ListTenants = "SYSTEM_LIST_TENANTS";
public const string CreateTenantUser = "SYS_CREATE_USER";
public const string CreateTenantUserRejected = "SYS_CREATE_USER_ERR";
}

View File

@ -1,115 +0,0 @@
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text.Json;
using System.Text.Json.Serialization;
using line_gestao_api.Data;
using line_gestao_api.Models;
namespace line_gestao_api.Services;
public class SystemAuditService : ISystemAuditService
{
private const int ActionMaxLength = 20;
private static readonly JsonSerializerOptions JsonOptions = new()
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};
private readonly AppDbContext _db;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly ITenantProvider _tenantProvider;
public SystemAuditService(
AppDbContext db,
IHttpContextAccessor httpContextAccessor,
ITenantProvider tenantProvider)
{
_db = db;
_httpContextAccessor = httpContextAccessor;
_tenantProvider = tenantProvider;
}
public async Task LogAsync(string action, Guid targetTenantId, object? metadata = null, CancellationToken cancellationToken = default)
{
var actorTenantId = _tenantProvider.ActorTenantId;
if (!actorTenantId.HasValue || actorTenantId.Value == Guid.Empty)
{
return;
}
var user = _httpContextAccessor.HttpContext?.User;
var userId = ResolveUserId(user);
var userName = ResolveUserName(user);
var userEmail = ResolveUserEmail(user);
var request = _httpContextAccessor.HttpContext?.Request;
var requestPath = request?.Path.Value;
var requestMethod = request?.Method;
var ipAddress = _httpContextAccessor.HttpContext?.Connection?.RemoteIpAddress?.ToString();
var safeMetadataJson = JsonSerializer.Serialize(metadata ?? new { }, JsonOptions);
var normalizedAction = NormalizeAction(action);
_db.AuditLogs.Add(new AuditLog
{
TenantId = actorTenantId.Value,
ActorUserId = userId,
ActorTenantId = actorTenantId.Value,
TargetTenantId = targetTenantId,
OccurredAtUtc = DateTime.UtcNow,
Action = normalizedAction,
Page = "System",
EntityName = "System",
EntityId = targetTenantId.ToString(),
EntityLabel = null,
ChangesJson = "[]",
MetadataJson = safeMetadataJson,
UserId = userId,
UserName = userName,
UserEmail = userEmail,
RequestPath = requestPath,
RequestMethod = requestMethod,
IpAddress = ipAddress
});
await _db.SaveChangesAsync(cancellationToken);
}
private static string NormalizeAction(string? action)
{
var normalized = (action ?? string.Empty).Trim().ToUpperInvariant();
if (string.IsNullOrEmpty(normalized))
{
return "UNKNOWN";
}
if (normalized.Length <= ActionMaxLength)
{
return normalized;
}
return normalized[..ActionMaxLength];
}
private static Guid? ResolveUserId(ClaimsPrincipal? user)
{
var raw = user?.FindFirstValue(ClaimTypes.NameIdentifier)
?? user?.FindFirstValue(JwtRegisteredClaimNames.Sub)
?? user?.FindFirstValue("sub");
return Guid.TryParse(raw, out var parsed) ? parsed : null;
}
private static string? ResolveUserName(ClaimsPrincipal? user)
{
return user?.FindFirstValue("name")
?? user?.FindFirstValue(ClaimTypes.Name)
?? user?.Identity?.Name;
}
private static string? ResolveUserEmail(ClaimsPrincipal? user)
{
return user?.FindFirstValue(ClaimTypes.Email)
?? user?.FindFirstValue(JwtRegisteredClaimNames.Email)
?? user?.FindFirstValue("email");
}
}

View File

@ -1,11 +0,0 @@
namespace line_gestao_api.Services;
public static class SystemTenantConstants
{
public const string SystemTenantSeed = "SYSTEM_TENANT";
public const string SystemTenantNomeOficial = "SystemTenant";
public const string SystemRole = AppRoles.SysAdmin;
public const string MobileLinesClienteSourceType = "MobileLines.Cliente";
public static readonly Guid SystemTenantId = DeterministicGuid.FromString(SystemTenantSeed);
}

View File

@ -13,14 +13,8 @@ public class TenantProvider : ITenantProvider
_httpContextAccessor = httpContextAccessor; _httpContextAccessor = httpContextAccessor;
} }
public Guid? ActorTenantId => TenantId;
public Guid? TenantId => CurrentTenant.Value ?? ResolveFromClaims(); public Guid? TenantId => CurrentTenant.Value ?? ResolveFromClaims();
public bool HasGlobalViewAccess =>
HasRole(AppRoles.SysAdmin) ||
HasRole(AppRoles.Gestor);
public void SetTenantId(Guid? tenantId) public void SetTenantId(Guid? tenantId)
{ {
CurrentTenant.Value = tenantId; CurrentTenant.Value = tenantId;
@ -33,21 +27,4 @@ public class TenantProvider : ITenantProvider
return Guid.TryParse(claim, out var tenantId) ? tenantId : null; return Guid.TryParse(claim, out var tenantId) ? tenantId : null;
} }
private bool HasRole(string role)
{
var principal = _httpContextAccessor.HttpContext?.User;
if (principal?.Identity?.IsAuthenticated != true)
{
return false;
}
var roleClaims = principal.FindAll(ClaimTypes.Role)
.Select(c => c.Value)
.Concat(principal.FindAll("role").Select(c => c.Value))
.Concat(principal.FindAll("roles").Select(c => c.Value))
.Distinct(StringComparer.OrdinalIgnoreCase);
return roleClaims.Any(r => string.Equals(r, role, StringComparison.OrdinalIgnoreCase));
}
} }

View File

@ -49,10 +49,7 @@ public class VigenciaNotificationBackgroundService : BackgroundService
return; return;
} }
var tenants = await db.Tenants var tenants = await db.Tenants.AsNoTracking().ToListAsync(stoppingToken);
.AsNoTracking()
.Where(t => !t.IsSystem && t.Ativo)
.ToListAsync(stoppingToken);
if (tenants.Count == 0) if (tenants.Count == 0)
{ {
_logger.LogWarning("Nenhum tenant encontrado para gerar notificações."); _logger.LogWarning("Nenhum tenant encontrado para gerar notificações.");

View File

@ -83,8 +83,6 @@ public class VigenciaNotificationSyncService : IVigenciaNotificationSyncService
.Where(u => !string.IsNullOrWhiteSpace(u.Email)) .Where(u => !string.IsNullOrWhiteSpace(u.Email))
.ToDictionary(u => u.Email!.Trim().ToLowerInvariant(), u => u.Id); .ToDictionary(u => u.Email!.Trim().ToLowerInvariant(), u => u.Id);
await ApplyAutoRenewalsAsync(tenantId, today, userByName, userByEmail, cancellationToken);
var vigencias = await _db.VigenciaLines.AsNoTracking() var vigencias = await _db.VigenciaLines.AsNoTracking()
.Where(v => v.DtTerminoFidelizacao != null) .Where(v => v.DtTerminoFidelizacao != null)
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
@ -215,112 +213,6 @@ public class VigenciaNotificationSyncService : IVigenciaNotificationSyncService
await _db.SaveChangesAsync(cancellationToken); await _db.SaveChangesAsync(cancellationToken);
} }
private async Task ApplyAutoRenewalsAsync(
Guid tenantId,
DateTime todayUtc,
IReadOnlyDictionary<string, Guid> userByName,
IReadOnlyDictionary<string, Guid> userByEmail,
CancellationToken cancellationToken)
{
var scheduledLines = await _db.VigenciaLines
.Where(v =>
v.AutoRenewYears != null &&
v.AutoRenewReferenceEndDate != null &&
v.DtTerminoFidelizacao != null)
.ToListAsync(cancellationToken);
if (scheduledLines.Count == 0)
{
return;
}
var changed = false;
var autoRenewNotifications = new List<Notification>();
var nowUtc = DateTime.UtcNow;
foreach (var vigencia in scheduledLines)
{
var years = NormalizeAutoRenewYears(vigencia.AutoRenewYears);
if (!years.HasValue || !vigencia.DtTerminoFidelizacao.HasValue || !vigencia.AutoRenewReferenceEndDate.HasValue)
{
if (vigencia.AutoRenewYears.HasValue || vigencia.AutoRenewReferenceEndDate.HasValue || vigencia.AutoRenewConfiguredAt.HasValue)
{
ClearAutoRenewSchedule(vigencia);
vigencia.UpdatedAt = nowUtc;
changed = true;
}
continue;
}
var currentEndUtc = ToUtcDate(vigencia.DtTerminoFidelizacao.Value);
var referenceEndUtc = ToUtcDate(vigencia.AutoRenewReferenceEndDate.Value);
// As datas de vigência foram alteradas manualmente após o agendamento:
// não renova automaticamente e limpa o agendamento.
if (currentEndUtc != referenceEndUtc)
{
ClearAutoRenewSchedule(vigencia);
vigencia.UpdatedAt = nowUtc;
changed = true;
continue;
}
// Só executa a renovação no vencimento (ou se já passou e segue sem alteração manual).
if (currentEndUtc > todayUtc)
{
continue;
}
var newStartUtc = currentEndUtc.AddDays(1);
var newEndUtc = currentEndUtc.AddYears(years.Value);
vigencia.DtEfetivacaoServico = newStartUtc;
vigencia.DtTerminoFidelizacao = newEndUtc;
vigencia.LastAutoRenewedAt = nowUtc;
ClearAutoRenewSchedule(vigencia);
vigencia.UpdatedAt = nowUtc;
changed = true;
autoRenewNotifications.Add(BuildAutoRenewNotification(
vigencia,
years.Value,
currentEndUtc,
newEndUtc,
ResolveUserId(vigencia.Usuario, userByName, userByEmail),
tenantId));
}
if (!changed && autoRenewNotifications.Count == 0)
{
return;
}
if (autoRenewNotifications.Count > 0)
{
var dedupKeys = autoRenewNotifications
.Select(n => n.DedupKey)
.Distinct(StringComparer.Ordinal)
.ToList();
var existingDedupKeys = await _db.Notifications.AsNoTracking()
.Where(n => dedupKeys.Contains(n.DedupKey))
.Select(n => n.DedupKey)
.ToListAsync(cancellationToken);
var existingSet = existingDedupKeys.ToHashSet(StringComparer.Ordinal);
autoRenewNotifications = autoRenewNotifications
.Where(n => !existingSet.Contains(n.DedupKey))
.ToList();
if (autoRenewNotifications.Count > 0)
{
await _db.Notifications.AddRangeAsync(autoRenewNotifications, cancellationToken);
}
}
await _db.SaveChangesAsync(cancellationToken);
}
private async Task CleanupOutdatedNotificationsAsync( private async Task CleanupOutdatedNotificationsAsync(
IReadOnlyCollection<VigenciaLine> vigencias, IReadOnlyCollection<VigenciaLine> vigencias,
bool notifyAllFutureDates, bool notifyAllFutureDates,
@ -457,38 +349,6 @@ public class VigenciaNotificationSyncService : IVigenciaNotificationSyncService
}; };
} }
private static Notification BuildAutoRenewNotification(
VigenciaLine vigencia,
int years,
DateTime previousEndUtc,
DateTime newEndUtc,
Guid? userId,
Guid tenantId)
{
var linha = vigencia.Linha?.Trim();
var cliente = vigencia.Cliente?.Trim();
var usuario = vigencia.Usuario?.Trim();
var dedupKey = BuildAutoRenewDedupKey(tenantId, vigencia.Id, previousEndUtc, years);
return new Notification
{
Tipo = "RenovacaoAutomatica",
Titulo = $"Renovação automática concluída{FormatLinha(linha)}",
Mensagem = $"A linha{FormatLinha(linha)} do cliente {cliente ?? "(sem cliente)"} foi renovada automaticamente por {years} ano(s): {previousEndUtc:dd/MM/yyyy} → {newEndUtc:dd/MM/yyyy}.",
Data = DateTime.UtcNow,
ReferenciaData = newEndUtc,
DiasParaVencer = null,
Lida = false,
DedupKey = dedupKey,
UserId = userId,
Usuario = usuario,
Cliente = cliente,
Linha = linha,
VigenciaLineId = vigencia.Id,
TenantId = tenantId
};
}
private static string BuildDedupKey( private static string BuildDedupKey(
string tipo, string tipo,
DateTime referenciaData, DateTime referenciaData,
@ -510,59 +370,6 @@ public class VigenciaNotificationSyncService : IVigenciaNotificationSyncService
return string.Join('|', parts); return string.Join('|', parts);
} }
private static string BuildAutoRenewDedupKey(Guid tenantId, Guid vigenciaLineId, DateTime referenceEndDateUtc, int years)
{
return string.Join('|', new[]
{
"renovacaoautomatica",
tenantId.ToString("N"),
vigenciaLineId.ToString("N"),
referenceEndDateUtc.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture),
years.ToString(CultureInfo.InvariantCulture)
});
}
private static Guid? ResolveUserId(
string? usuario,
IReadOnlyDictionary<string, Guid> userByName,
IReadOnlyDictionary<string, Guid> userByEmail)
{
var key = usuario?.Trim().ToLowerInvariant();
if (string.IsNullOrWhiteSpace(key))
{
return null;
}
if (userByEmail.TryGetValue(key, out var byEmail))
{
return byEmail;
}
if (userByName.TryGetValue(key, out var byName))
{
return byName;
}
return null;
}
private static int? NormalizeAutoRenewYears(int? years)
{
return years == 2 ? years : null;
}
private static DateTime ToUtcDate(DateTime value)
{
return DateTime.SpecifyKind(value.Date, DateTimeKind.Utc);
}
private static void ClearAutoRenewSchedule(VigenciaLine vigencia)
{
vigencia.AutoRenewYears = null;
vigencia.AutoRenewReferenceEndDate = null;
vigencia.AutoRenewConfiguredAt = null;
}
private static string FormatLinha(string? linha) private static string FormatLinha(string? linha)
{ {
return string.IsNullOrWhiteSpace(linha) ? string.Empty : $" {linha}"; return string.IsNullOrWhiteSpace(linha) ? string.Empty : $" {linha}";

View File

@ -1,6 +1,6 @@
{ {
"ConnectionStrings": { "ConnectionStrings": {
"Default": "Host=localhost;Port=5432;Database=linegestao;Username=postgres;Password=postgres" "Default": "Host=localhost;Port=5432;Database=linegestao;Username=linegestao_app;Password=1231234512Ed@"
}, },
"Jwt": { "Jwt": {
"Key": "linegestao-local-jwt-key-2026-dev-strong-123456789", "Key": "linegestao-local-jwt-key-2026-dev-strong-123456789",
@ -31,8 +31,9 @@
"Seed": { "Seed": {
"Enabled": true, "Enabled": true,
"ReapplyAdminCredentialsOnStartup": true, "ReapplyAdminCredentialsOnStartup": true,
"AdminMasterName": "Admin Master", "DefaultTenantName": "Default",
"AdminMasterEmail": "admin.master@linegestao.local", "AdminName": "Administrador",
"AdminMasterPassword": "DevAdminMaster123!" "AdminEmail": "admin@linegestao.local",
"AdminPassword": "DevAdmin123!"
} }
} }

View File

@ -1,6 +1,6 @@
{ {
"ConnectionStrings": { "ConnectionStrings": {
"Default": "Host=localhost;Port=5432;Database=linegestao;Username=postgres;Password=postgres" "Default": "Host=localhost;Port=5432;Database=linegestao;Username=linegestao_app;Password=1231234512Ed@"
}, },
"Jwt": { "Jwt": {
"Key": "YOUR_LOCAL_STRONG_JWT_KEY_MIN_32_CHARS", "Key": "YOUR_LOCAL_STRONG_JWT_KEY_MIN_32_CHARS",
@ -11,8 +11,9 @@
"Seed": { "Seed": {
"Enabled": true, "Enabled": true,
"ReapplyAdminCredentialsOnStartup": false, "ReapplyAdminCredentialsOnStartup": false,
"AdminMasterName": "Admin Master", "DefaultTenantName": "Default",
"AdminMasterEmail": "admin.master@linegestao.local", "AdminName": "Administrador",
"AdminMasterPassword": "YOUR_LOCAL_ADMIN_MASTER_SEED_PASSWORD" "AdminEmail": "admin@linegestao.local",
"AdminPassword": "YOUR_LOCAL_ADMIN_SEED_PASSWORD"
} }
} }

View File

@ -1,6 +1,6 @@
{ {
"ConnectionStrings": { "ConnectionStrings": {
"Default": "Host=localhost;Port=5432;Database=linegestao;Username=postgres;Password=postgres" "Default": "Host=localhost;Port=5432;Database=linegestao;Username=linegestao_app;Password=1231234512Ed@"
}, },
"Jwt": { "Jwt": {
"Key": "linegestao-local-jwt-key-2026-dev-strong-123456789", "Key": "linegestao-local-jwt-key-2026-dev-strong-123456789",
@ -31,8 +31,9 @@
"Seed": { "Seed": {
"Enabled": true, "Enabled": true,
"ReapplyAdminCredentialsOnStartup": true, "ReapplyAdminCredentialsOnStartup": true,
"AdminMasterName": "Admin Master", "DefaultTenantName": "Default",
"AdminMasterEmail": "admin.master@linegestao.local", "AdminName": "Administrador",
"AdminMasterPassword": "DevAdminMaster123!" "AdminEmail": "admin@linegestao.local",
"AdminPassword": "DevAdmin123!"
} }
} }

View File

@ -148,12 +148,8 @@ namespace line_gestao_api.Tests
TenantId = tenantId; TenantId = tenantId;
} }
public Guid? ActorTenantId => TenantId;
public Guid? TenantId { get; private set; } public Guid? TenantId { get; private set; }
public bool HasGlobalViewAccess => false;
public void SetTenantId(Guid? tenantId) public void SetTenantId(Guid? tenantId)
{ {
TenantId = tenantId; TenantId = tenantId;

View File

@ -1,335 +0,0 @@
using System.Net;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using line_gestao_api.Data;
using line_gestao_api.Dtos;
using line_gestao_api.Models;
using line_gestao_api.Services;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Hosting;
using Xunit;
namespace line_gestao_api.Tests;
public class SystemTenantIntegrationTests
{
private static readonly Guid TenantAId = Guid.Parse("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa");
private static readonly Guid TenantBId = Guid.Parse("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb");
private const string TenantAClientName = "CLIENTE-ALFA LTDA";
private const string TenantBClientName = "CLIENTE-BETA S/A";
[Fact]
public async Task CommonUser_OnlySeesOwnTenantData()
{
using var factory = new ApiFactory();
var client = factory.CreateClient();
await SeedTenantsAndLinesAsync(factory.Services);
await UpsertUserAsync(factory.Services, TenantAId, "tenanta.user@test.local", "TenantA123!", "cliente");
var token = await LoginAndGetTokenAsync(client, "tenanta.user@test.local", "TenantA123!", TenantAId);
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
var response = await client.GetAsync("/api/lines/clients");
response.EnsureSuccessStatusCode();
var clients = await response.Content.ReadFromJsonAsync<List<string>>();
Assert.NotNull(clients);
Assert.Contains(TenantAClientName, clients!);
Assert.DoesNotContain(TenantBClientName, clients);
}
[Fact]
public async Task CommonUser_CannotAccessSystemEndpoints()
{
using var factory = new ApiFactory();
var client = factory.CreateClient();
await SeedTenantsAndLinesAsync(factory.Services);
await UpsertUserAsync(factory.Services, TenantAId, "tenanta.block@test.local", "TenantA123!", "cliente");
var token = await LoginAndGetTokenAsync(client, "tenanta.block@test.local", "TenantA123!", TenantAId);
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
var response = await client.GetAsync("/api/system/tenants?source=MobileLines.Cliente&active=true");
Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
}
[Fact]
public async Task SysAdmin_CanListClientTenants()
{
using var factory = new ApiFactory();
var client = factory.CreateClient();
await SeedTenantsAndLinesAsync(factory.Services);
var token = await LoginAndGetTokenAsync(
client,
"admin.master@test.local",
"AdminMaster123!",
SystemTenantConstants.SystemTenantId);
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
var response = await client.GetAsync("/api/system/tenants?source=MobileLines.Cliente&active=true");
response.EnsureSuccessStatusCode();
var tenants = await response.Content.ReadFromJsonAsync<List<SystemTenantListItemDto>>();
Assert.NotNull(tenants);
Assert.Contains(tenants!, t => t.TenantId == TenantAId && t.NomeOficial == TenantAClientName);
Assert.Contains(tenants, t => t.TenantId == TenantBId && t.NomeOficial == TenantBClientName);
}
[Fact]
public async Task SysAdmin_CreatesTenantUser_AndNewUserSeesOnlyOwnTenant()
{
using var factory = new ApiFactory();
var client = factory.CreateClient();
await SeedTenantsAndLinesAsync(factory.Services);
var adminToken = await LoginAndGetTokenAsync(
client,
"admin.master@test.local",
"AdminMaster123!",
SystemTenantConstants.SystemTenantId);
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", adminToken);
var request = new CreateSystemTenantUserRequest
{
Name = "Usuário Cliente A",
Email = "novo.clientea@test.local",
Password = "ClienteA123!",
Roles = new List<string> { "cliente" }
};
var createResponse = await client.PostAsJsonAsync($"/api/system/tenants/{TenantAId}/users", request);
createResponse.EnsureSuccessStatusCode();
var created = await createResponse.Content.ReadFromJsonAsync<SystemTenantUserCreatedDto>();
Assert.NotNull(created);
Assert.Equal(TenantAId, created!.TenantId);
Assert.Equal("novo.clientea@test.local", created.Email);
Assert.Contains("cliente", created.Roles);
var userToken = await LoginAndGetTokenAsync(client, "novo.clientea@test.local", "ClienteA123!", TenantAId);
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", userToken);
var visibleClientsResponse = await client.GetAsync("/api/lines/clients");
visibleClientsResponse.EnsureSuccessStatusCode();
var clients = await visibleClientsResponse.Content.ReadFromJsonAsync<List<string>>();
Assert.NotNull(clients);
Assert.Contains(TenantAClientName, clients!);
Assert.DoesNotContain(TenantBClientName, clients);
await using var scope = factory.Services.CreateAsyncScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
var systemAudit = await db.AuditLogs
.IgnoreQueryFilters()
.OrderByDescending(x => x.OccurredAtUtc)
.FirstOrDefaultAsync(x => x.Action == SystemAuditActions.CreateTenantUser);
Assert.NotNull(systemAudit);
Assert.Equal(SystemTenantConstants.SystemTenantId, systemAudit!.ActorTenantId);
Assert.Equal(TenantAId, systemAudit.TargetTenantId);
Assert.DoesNotContain("ClienteA123!", systemAudit.MetadataJson);
}
private static async Task SeedTenantsAndLinesAsync(IServiceProvider services)
{
await using var scope = services.CreateAsyncScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
var tenantA = await db.Tenants.FirstOrDefaultAsync(t => t.Id == TenantAId);
if (tenantA == null)
{
db.Tenants.Add(new Tenant
{
Id = TenantAId,
NomeOficial = TenantAClientName,
IsSystem = false,
Ativo = true,
SourceType = SystemTenantConstants.MobileLinesClienteSourceType,
SourceKey = TenantAClientName,
CreatedAt = DateTime.UtcNow
});
}
var tenantB = await db.Tenants.FirstOrDefaultAsync(t => t.Id == TenantBId);
if (tenantB == null)
{
db.Tenants.Add(new Tenant
{
Id = TenantBId,
NomeOficial = TenantBClientName,
IsSystem = false,
Ativo = true,
SourceType = SystemTenantConstants.MobileLinesClienteSourceType,
SourceKey = TenantBClientName,
CreatedAt = DateTime.UtcNow
});
}
var currentLines = await db.MobileLines.IgnoreQueryFilters().ToListAsync();
if (currentLines.Count > 0)
{
db.MobileLines.RemoveRange(currentLines);
}
db.MobileLines.AddRange(
new MobileLine
{
Id = Guid.NewGuid(),
Item = 1,
Linha = "5511999990001",
Cliente = TenantAClientName,
TenantId = TenantAId
},
new MobileLine
{
Id = Guid.NewGuid(),
Item = 2,
Linha = "5511888880002",
Cliente = TenantBClientName,
TenantId = TenantBId
});
await db.SaveChangesAsync();
}
private static async Task UpsertUserAsync(
IServiceProvider services,
Guid tenantId,
string email,
string password,
params string[] roles)
{
await using var scope = services.CreateAsyncScope();
var userManager = scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>();
var tenantProvider = scope.ServiceProvider.GetRequiredService<ITenantProvider>();
var previousTenant = tenantProvider.ActorTenantId;
tenantProvider.SetTenantId(tenantId);
try
{
var normalizedEmail = userManager.NormalizeEmail(email);
var user = await userManager.Users
.IgnoreQueryFilters()
.FirstOrDefaultAsync(u => u.TenantId == tenantId && u.NormalizedEmail == normalizedEmail);
if (user == null)
{
user = new ApplicationUser
{
Name = email,
Email = email,
UserName = email,
TenantId = tenantId,
EmailConfirmed = true,
IsActive = true,
LockoutEnabled = true
};
var createResult = await userManager.CreateAsync(user, password);
Assert.True(createResult.Succeeded, string.Join("; ", createResult.Errors.Select(e => e.Description)));
}
else
{
var resetToken = await userManager.GeneratePasswordResetTokenAsync(user);
var reset = await userManager.ResetPasswordAsync(user, resetToken, password);
Assert.True(reset.Succeeded, string.Join("; ", reset.Errors.Select(e => e.Description)));
}
var existingRoles = await userManager.GetRolesAsync(user);
if (existingRoles.Count > 0)
{
var removeRolesResult = await userManager.RemoveFromRolesAsync(user, existingRoles);
Assert.True(removeRolesResult.Succeeded, string.Join("; ", removeRolesResult.Errors.Select(e => e.Description)));
}
var addRolesResult = await userManager.AddToRolesAsync(user, roles);
Assert.True(addRolesResult.Succeeded, string.Join("; ", addRolesResult.Errors.Select(e => e.Description)));
}
finally
{
tenantProvider.SetTenantId(previousTenant);
}
}
private static async Task<string> LoginAndGetTokenAsync(HttpClient client, string email, string password, Guid tenantId)
{
var previousAuth = client.DefaultRequestHeaders.Authorization;
client.DefaultRequestHeaders.Authorization = null;
try
{
var response = await client.PostAsJsonAsync("/auth/login", new
{
email,
password,
tenantId
});
response.EnsureSuccessStatusCode();
var auth = await response.Content.ReadFromJsonAsync<AuthResponse>();
Assert.NotNull(auth);
Assert.False(string.IsNullOrWhiteSpace(auth!.Token));
return auth.Token;
}
finally
{
client.DefaultRequestHeaders.Authorization = previousAuth;
}
}
private sealed class ApiFactory : WebApplicationFactory<Program>
{
private readonly string _databaseName = $"line-gestao-tests-{Guid.NewGuid()}";
protected override void ConfigureWebHost(Microsoft.AspNetCore.Hosting.IWebHostBuilder builder)
{
builder.UseEnvironment("Testing");
builder.ConfigureAppConfiguration((_, config) =>
{
config.AddInMemoryCollection(new Dictionary<string, string?>
{
["App:UseHttpsRedirection"] = "false",
["Seed:Enabled"] = "true",
["Seed:AdminMasterName"] = "Admin Master",
["Seed:AdminMasterEmail"] = "admin.master@test.local",
["Seed:AdminMasterPassword"] = "AdminMaster123!",
["Seed:ReapplyAdminCredentialsOnStartup"] = "true"
});
});
builder.ConfigureServices(services =>
{
var notificationHostedService = services
.Where(d => d.ServiceType == typeof(IHostedService) &&
d.ImplementationType == typeof(VigenciaNotificationBackgroundService))
.ToList();
foreach (var descriptor in notificationHostedService)
{
services.Remove(descriptor);
}
services.RemoveAll<AppDbContext>();
services.RemoveAll<DbContextOptions<AppDbContext>>();
services.AddDbContext<AppDbContext>(options =>
{
options.UseInMemoryDatabase(_databaseName);
});
});
}
}
}

View File

@ -8,7 +8,6 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="10.0.1" /> <PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="10.0.1" />
<PackageReference Include="xunit" Version="2.9.2" /> <PackageReference Include="xunit" Version="2.9.2" />

View File

@ -1,395 +0,0 @@
{
"info": {
"name": "Line Gestao - SystemTenant Multi-tenant Tests",
"_postman_id": "c4c0b7d9-7f11-4a0c-b8ca-332633f12601",
"description": "Fluxo de testes para sysadmin, endpoints /api/system/* e isolamento por tenant.",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
},
"variable": [
{ "key": "adminMasterToken", "value": "" },
{ "key": "tenantAUserToken", "value": "" },
{ "key": "newTenantAUserToken", "value": "" },
{ "key": "tenantAUserId", "value": "" },
{ "key": "newTenantAUserId", "value": "" },
{ "key": "newTenantAUserEmail", "value": "" },
{ "key": "newTenantAUserPassword", "value": "" },
{ "key": "newTenantAUserName", "value": "" }
],
"item": [
{
"name": "1) Login sysadmin",
"request": {
"method": "POST",
"header": [
{ "key": "Content-Type", "value": "application/json" }
],
"body": {
"mode": "raw",
"raw": "{\n \"email\": \"{{adminMasterEmail}}\",\n \"password\": \"{{adminMasterPassword}}\",\n \"tenantId\": \"{{systemTenantId}}\"\n}"
},
"url": {
"raw": "{{baseUrl}}/auth/login",
"host": ["{{baseUrl}}"],
"path": ["auth", "login"]
}
},
"event": [
{
"listen": "test",
"script": {
"type": "text/javascript",
"exec": [
"pm.test('Status 200', function () {",
" pm.response.to.have.status(200);",
"});",
"const json = pm.response.json();",
"pm.test('Retorna token JWT', function () {",
" pm.expect(json.token).to.be.a('string').and.not.empty;",
"});",
"pm.collectionVariables.set('adminMasterToken', json.token);"
]
}
}
]
},
{
"name": "2) GET /api/system/tenants (sysadmin)",
"request": {
"auth": {
"type": "bearer",
"bearer": [
{ "key": "token", "value": "{{adminMasterToken}}", "type": "string" }
]
},
"method": "GET",
"url": {
"raw": "{{baseUrl}}/api/system/tenants?source=MobileLines.Cliente&active=true",
"host": ["{{baseUrl}}"],
"path": ["api", "system", "tenants"],
"query": [
{ "key": "source", "value": "MobileLines.Cliente" },
{ "key": "active", "value": "true" }
]
}
},
"event": [
{
"listen": "test",
"script": {
"type": "text/javascript",
"exec": [
"pm.test('Status 200', function () {",
" pm.response.to.have.status(200);",
"});",
"const tenants = pm.response.json();",
"pm.test('Retorna array de tenants', function () {",
" pm.expect(Array.isArray(tenants)).to.eql(true);",
"});",
"const tenantAClientName = pm.environment.get('tenantAClientName');",
"const tenantBClientName = pm.environment.get('tenantBClientName');",
"if (tenantAClientName) {",
" const tenantA = tenants.find(t => t.nomeOficial === tenantAClientName || t.NomeOficial === tenantAClientName);",
" pm.test('Tenant A encontrado por nomeOficial', function () {",
" pm.expect(tenantA).to.exist;",
" });",
" if (tenantA && (tenantA.tenantId || tenantA.TenantId)) {",
" pm.environment.set('tenantAId', tenantA.tenantId || tenantA.TenantId);",
" }",
"}",
"if (tenantBClientName) {",
" const tenantB = tenants.find(t => t.nomeOficial === tenantBClientName || t.NomeOficial === tenantBClientName);",
" pm.test('Tenant B encontrado por nomeOficial', function () {",
" pm.expect(tenantB).to.exist;",
" });",
" if (tenantB && (tenantB.tenantId || tenantB.TenantId)) {",
" pm.environment.set('tenantBId', tenantB.tenantId || tenantB.TenantId);",
" }",
"}"
]
}
}
]
},
{
"name": "3) POST /api/system/tenants/{tenantId}/users (criar usuário comum tenant A)",
"request": {
"auth": {
"type": "bearer",
"bearer": [
{ "key": "token", "value": "{{adminMasterToken}}", "type": "string" }
]
},
"method": "POST",
"header": [
{ "key": "Content-Type", "value": "application/json" }
],
"body": {
"mode": "raw",
"raw": "{\n \"name\": \"{{tenantAUserName}}\",\n \"email\": \"{{tenantAUserEmail}}\",\n \"password\": \"{{tenantAUserPassword}}\",\n \"roles\": [\"cliente\"]\n}"
},
"url": {
"raw": "{{baseUrl}}/api/system/tenants/{{tenantAId}}/users",
"host": ["{{baseUrl}}"],
"path": ["api", "system", "tenants", "{{tenantAId}}", "users"]
}
},
"event": [
{
"listen": "test",
"script": {
"type": "text/javascript",
"exec": [
"pm.test('Status 201 (criado) ou 409 (já existe)', function () {",
" pm.expect([201, 409]).to.include(pm.response.code);",
"});",
"if (pm.response.code === 201) {",
" const json = pm.response.json();",
" pm.collectionVariables.set('tenantAUserId', json.userId || json.UserId || '');",
"}"
]
}
}
]
},
{
"name": "4) Login usuário comum tenant A",
"request": {
"method": "POST",
"header": [
{ "key": "Content-Type", "value": "application/json" }
],
"body": {
"mode": "raw",
"raw": "{\n \"email\": \"{{tenantAUserEmail}}\",\n \"password\": \"{{tenantAUserPassword}}\",\n \"tenantId\": \"{{tenantAId}}\"\n}"
},
"url": {
"raw": "{{baseUrl}}/auth/login",
"host": ["{{baseUrl}}"],
"path": ["auth", "login"]
}
},
"event": [
{
"listen": "test",
"script": {
"type": "text/javascript",
"exec": [
"pm.test('Status 200', function () {",
" pm.response.to.have.status(200);",
"});",
"const json = pm.response.json();",
"pm.collectionVariables.set('tenantAUserToken', json.token);"
]
}
}
]
},
{
"name": "5) Usuário comum NÃO acessa /api/system/*",
"request": {
"auth": {
"type": "bearer",
"bearer": [
{ "key": "token", "value": "{{tenantAUserToken}}", "type": "string" }
]
},
"method": "GET",
"url": {
"raw": "{{baseUrl}}/api/system/tenants?source=MobileLines.Cliente&active=true",
"host": ["{{baseUrl}}"],
"path": ["api", "system", "tenants"],
"query": [
{ "key": "source", "value": "MobileLines.Cliente" },
{ "key": "active", "value": "true" }
]
}
},
"event": [
{
"listen": "test",
"script": {
"type": "text/javascript",
"exec": [
"pm.test('Status 403 Forbidden', function () {",
" pm.response.to.have.status(403);",
"});"
]
}
}
]
},
{
"name": "6) Usuário comum tenant A vê apenas seu tenant",
"request": {
"auth": {
"type": "bearer",
"bearer": [
{ "key": "token", "value": "{{tenantAUserToken}}", "type": "string" }
]
},
"method": "GET",
"url": {
"raw": "{{baseUrl}}/api/lines/clients",
"host": ["{{baseUrl}}"],
"path": ["api", "lines", "clients"]
}
},
"event": [
{
"listen": "test",
"script": {
"type": "text/javascript",
"exec": [
"pm.test('Status 200', function () {",
" pm.response.to.have.status(200);",
"});",
"const clients = pm.response.json();",
"pm.test('Retorna lista de clientes', function () {",
" pm.expect(Array.isArray(clients)).to.eql(true);",
"});",
"const tenantAClientName = pm.environment.get('tenantAClientName');",
"const tenantBClientName = pm.environment.get('tenantBClientName');",
"if (tenantAClientName) {",
" pm.test('Contém cliente do tenant A', function () {",
" pm.expect(clients).to.include(tenantAClientName);",
" });",
"}",
"if (tenantBClientName) {",
" pm.test('Não contém cliente do tenant B', function () {",
" pm.expect(clients).to.not.include(tenantBClientName);",
" });",
"}"
]
}
}
]
},
{
"name": "7) POST /api/system/tenants/{tenantId}/users (novo usuário tenant A)",
"event": [
{
"listen": "prerequest",
"script": {
"type": "text/javascript",
"exec": [
"const suffix = Date.now().toString().slice(-8);",
"pm.collectionVariables.set('newTenantAUserEmail', `novo.tenant.a.${suffix}@test.local`);",
"pm.collectionVariables.set('newTenantAUserPassword', 'ClienteA123!');",
"pm.collectionVariables.set('newTenantAUserName', `Novo Tenant A ${suffix}`);"
]
}
},
{
"listen": "test",
"script": {
"type": "text/javascript",
"exec": [
"pm.test('Status 201', function () {",
" pm.response.to.have.status(201);",
"});",
"const json = pm.response.json();",
"pm.collectionVariables.set('newTenantAUserId', json.userId || json.UserId || '');"
]
}
}
],
"request": {
"auth": {
"type": "bearer",
"bearer": [
{ "key": "token", "value": "{{adminMasterToken}}", "type": "string" }
]
},
"method": "POST",
"header": [
{ "key": "Content-Type", "value": "application/json" }
],
"body": {
"mode": "raw",
"raw": "{\n \"name\": \"{{newTenantAUserName}}\",\n \"email\": \"{{newTenantAUserEmail}}\",\n \"password\": \"{{newTenantAUserPassword}}\",\n \"roles\": [\"cliente\"]\n}"
},
"url": {
"raw": "{{baseUrl}}/api/system/tenants/{{tenantAId}}/users",
"host": ["{{baseUrl}}"],
"path": ["api", "system", "tenants", "{{tenantAId}}", "users"]
}
}
},
{
"name": "8) Login novo usuário tenant A",
"request": {
"method": "POST",
"header": [
{ "key": "Content-Type", "value": "application/json" }
],
"body": {
"mode": "raw",
"raw": "{\n \"email\": \"{{newTenantAUserEmail}}\",\n \"password\": \"{{newTenantAUserPassword}}\",\n \"tenantId\": \"{{tenantAId}}\"\n}"
},
"url": {
"raw": "{{baseUrl}}/auth/login",
"host": ["{{baseUrl}}"],
"path": ["auth", "login"]
}
},
"event": [
{
"listen": "test",
"script": {
"type": "text/javascript",
"exec": [
"pm.test('Status 200', function () {",
" pm.response.to.have.status(200);",
"});",
"const json = pm.response.json();",
"pm.collectionVariables.set('newTenantAUserToken', json.token);"
]
}
}
]
},
{
"name": "9) Novo usuário tenant A vê apenas seu tenant",
"request": {
"auth": {
"type": "bearer",
"bearer": [
{ "key": "token", "value": "{{newTenantAUserToken}}", "type": "string" }
]
},
"method": "GET",
"url": {
"raw": "{{baseUrl}}/api/lines/clients",
"host": ["{{baseUrl}}"],
"path": ["api", "lines", "clients"]
}
},
"event": [
{
"listen": "test",
"script": {
"type": "text/javascript",
"exec": [
"pm.test('Status 200', function () {",
" pm.response.to.have.status(200);",
"});",
"const clients = pm.response.json();",
"const tenantAClientName = pm.environment.get('tenantAClientName');",
"const tenantBClientName = pm.environment.get('tenantBClientName');",
"if (tenantAClientName) {",
" pm.test('Contém cliente do tenant A', function () {",
" pm.expect(clients).to.include(tenantAClientName);",
" });",
"}",
"if (tenantBClientName) {",
" pm.test('Não contém cliente do tenant B', function () {",
" pm.expect(clients).to.not.include(tenantBClientName);",
" });",
"}"
]
}
}
]
}
]
}

View File

@ -1,64 +0,0 @@
{
"id": "d1d8e905-e4b8-40c5-a62e-afb27c59b685",
"name": "Line Gestao - Local",
"values": [
{
"key": "baseUrl",
"value": "http://localhost:5000",
"enabled": true
},
{
"key": "systemTenantId",
"value": "562617c4-90dc-cfce-ddf4-64b6284dc4f2",
"enabled": true
},
{
"key": "adminMasterEmail",
"value": "sysadmin@linegestao.local",
"enabled": true
},
{
"key": "adminMasterPassword",
"value": "",
"enabled": true
},
{
"key": "tenantAId",
"value": "",
"enabled": true
},
{
"key": "tenantBId",
"value": "",
"enabled": true
},
{
"key": "tenantAClientName",
"value": "",
"enabled": true
},
{
"key": "tenantBClientName",
"value": "",
"enabled": true
},
{
"key": "tenantAUserName",
"value": "Usuario Tenant A",
"enabled": true
},
{
"key": "tenantAUserEmail",
"value": "tenanta.user@test.local",
"enabled": true
},
{
"key": "tenantAUserPassword",
"value": "TenantA123!",
"enabled": true
}
],
"_postman_variable_scope": "environment",
"_postman_exported_at": "2026-02-26T12:00:00.000Z",
"_postman_exported_using": "Codex GPT-5"
}