Compare commits
No commits in common. "9d7306c395854440318c4683ef74f16de9f8d14c" and "5101c3665a4c51eebda4797ee5984f804959a4e6" have entirely different histories.
9d7306c395
...
5101c3665a
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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 != "")
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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,15 +433,14 @@ 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
|
var exists = await _userManager.Users.AnyAsync(u =>
|
||||||
.IgnoreQueryFilters()
|
|
||||||
.AnyAsync(u =>
|
|
||||||
u.Id != userId &&
|
u.Id != userId &&
|
||||||
u.TenantId == tenantId &&
|
u.TenantId == tenantId &&
|
||||||
u.NormalizedEmail == normalized);
|
u.NormalizedEmail == normalized);
|
||||||
|
|
@ -460,6 +450,7 @@ public class UsersController : ControllerBase
|
||||||
errors.Add(new ValidationErrorDto { Field = "email", Message = "E-mail já cadastrado." });
|
errors.Add(new ValidationErrorDto { Field = "email", Message = "E-mail já cadastrado." });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(req.Senha))
|
if (!string.IsNullOrWhiteSpace(req.Senha))
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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 "";
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
347
Data/SeedData.cs
347
Data/SeedData.cs
|
|
@ -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);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
systemTenant.NomeOficial = SystemTenantConstants.SystemTenantNomeOficial;
|
|
||||||
systemTenant.IsSystem = true;
|
|
||||||
systemTenant.Ativo = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
db.Tenants.Add(tenant);
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
var emailFromEnv = Environment.GetEnvironmentVariable("SYSADMIN_EMAIL")
|
|
||||||
?? 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 adminMasterPassword = passwordFromEnv ?? options.AdminMasterPassword ?? string.Empty;
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(adminMasterEmail) || string.IsNullOrWhiteSpace(adminMasterPassword))
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException(
|
|
||||||
"Credenciais do sysadmin ausentes. Defina SYSADMIN_EMAIL e SYSADMIN_PASSWORD (ou Seed:AdminMasterEmail/Seed:AdminMasterPassword).");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var normalizedEmail = userManager.NormalizeEmail(adminMasterEmail);
|
await NormalizeLegacyTenantDataAsync(db, tenant.Id);
|
||||||
|
|
||||||
var previousTenant = tenantProvider.TenantId;
|
tenantProvider.SetTenantId(tenant.Id);
|
||||||
tenantProvider.SetTenantId(systemTenantId);
|
|
||||||
|
|
||||||
try
|
var normalizedEmail = userManager.NormalizeEmail(options.AdminEmail);
|
||||||
{
|
var existingAdmin = await userManager.Users
|
||||||
var existingAdminMaster = await userManager.Users
|
.FirstOrDefaultAsync(u => u.NormalizedEmail == normalizedEmail && u.TenantId == tenant.Id);
|
||||||
.IgnoreQueryFilters()
|
|
||||||
.FirstOrDefaultAsync(u => u.TenantId == systemTenantId && u.NormalizedEmail == normalizedEmail);
|
|
||||||
|
|
||||||
if (existingAdminMaster == null)
|
if (existingAdmin == null)
|
||||||
{
|
{
|
||||||
var adminMaster = new ApplicationUser
|
var adminUser = new ApplicationUser
|
||||||
{
|
{
|
||||||
Name = options.AdminMasterName,
|
UserName = options.AdminEmail,
|
||||||
Email = adminMasterEmail,
|
Email = options.AdminEmail,
|
||||||
UserName = adminMasterEmail,
|
Name = options.AdminName,
|
||||||
TenantId = systemTenantId,
|
TenantId = tenant.Id,
|
||||||
EmailConfirmed = true,
|
EmailConfirmed = true,
|
||||||
IsActive = true,
|
IsActive = true,
|
||||||
LockoutEnabled = 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)
|
||||||
{
|
{
|
||||||
existingAdminMaster.Name = options.AdminMasterName;
|
existingAdmin.Name = options.AdminName;
|
||||||
existingAdminMaster.Email = adminMasterEmail;
|
existingAdmin.Email = options.AdminEmail;
|
||||||
existingAdminMaster.UserName = adminMasterEmail;
|
existingAdmin.UserName = options.AdminEmail;
|
||||||
existingAdminMaster.EmailConfirmed = true;
|
existingAdmin.EmailConfirmed = true;
|
||||||
existingAdminMaster.IsActive = true;
|
existingAdmin.IsActive = true;
|
||||||
existingAdminMaster.LockoutEnabled = true;
|
existingAdmin.LockoutEnabled = true;
|
||||||
|
|
||||||
var updateResult = await userManager.UpdateAsync(existingAdminMaster);
|
await userManager.SetLockoutEndDateAsync(existingAdmin, null);
|
||||||
EnsureIdentitySucceeded(updateResult, "Falha ao atualizar usuário sysadmin.");
|
await userManager.ResetAccessFailedCountAsync(existingAdmin);
|
||||||
|
await userManager.UpdateAsync(existingAdmin);
|
||||||
|
|
||||||
if (options.ReapplyAdminCredentialsOnStartup)
|
var resetToken = await userManager.GeneratePasswordResetTokenAsync(existingAdmin);
|
||||||
{
|
var resetPasswordResult = await userManager.ResetPasswordAsync(existingAdmin, resetToken, options.AdminPassword);
|
||||||
await userManager.SetLockoutEndDateAsync(existingAdminMaster, null);
|
|
||||||
await userManager.ResetAccessFailedCountAsync(existingAdminMaster);
|
|
||||||
|
|
||||||
var resetToken = await userManager.GeneratePasswordResetTokenAsync(existingAdminMaster);
|
|
||||||
var resetPasswordResult = await userManager.ResetPasswordAsync(existingAdminMaster, resetToken, adminMasterPassword);
|
|
||||||
if (!resetPasswordResult.Succeeded)
|
if (!resetPasswordResult.Succeeded)
|
||||||
{
|
{
|
||||||
var removePasswordResult = await userManager.RemovePasswordAsync(existingAdminMaster);
|
var removePasswordResult = await userManager.RemovePasswordAsync(existingAdmin);
|
||||||
if (removePasswordResult.Succeeded)
|
if (removePasswordResult.Succeeded)
|
||||||
{
|
{
|
||||||
var addPasswordResult = await userManager.AddPasswordAsync(existingAdminMaster, adminMasterPassword);
|
await userManager.AddPasswordAsync(existingAdmin, options.AdminPassword);
|
||||||
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)
|
await userManager.AddToRoleAsync(existingAdmin, "admin");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tenantProvider.SetTenantId(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task NormalizeLegacyTenantDataAsync(AppDbContext db, Guid defaultTenantId)
|
||||||
{
|
{
|
||||||
|
if (defaultTenantId == Guid.Empty)
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
var details = string.Join("; ", result.Errors.Select(e => e.Description));
|
await db.Users
|
||||||
throw new InvalidOperationException($"{message} Detalhes: {details}");
|
.IgnoreQueryFilters()
|
||||||
}
|
.Where(x => x.TenantId == Guid.Empty)
|
||||||
|
.ExecuteUpdateAsync(setters => setters.SetProperty(x => x.TenantId, defaultTenantId));
|
||||||
|
|
||||||
private static async Task MigrateLegacyRolesAsync(AppDbContext db, RoleManager<IdentityRole<Guid>> roleManager)
|
await db.MobileLines
|
||||||
{
|
.IgnoreQueryFilters()
|
||||||
await MigrateLegacyRoleAsync(db, roleManager, "admin_master", AppRoles.SysAdmin);
|
.Where(x => x.TenantId == Guid.Empty)
|
||||||
await MigrateLegacyRoleAsync(db, roleManager, "admin", AppRoles.SysAdmin);
|
.ExecuteUpdateAsync(setters => setters.SetProperty(x => x.TenantId, defaultTenantId));
|
||||||
await MigrateLegacyRoleAsync(db, roleManager, "leitura", AppRoles.Cliente);
|
|
||||||
await MigrateLegacyRoleAsync(db, roleManager, "operador", AppRoles.Cliente);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async Task MigrateLegacyRoleAsync(
|
await db.MuregLines
|
||||||
AppDbContext db,
|
.IgnoreQueryFilters()
|
||||||
RoleManager<IdentityRole<Guid>> roleManager,
|
.Where(x => x.TenantId == Guid.Empty)
|
||||||
string legacyRole,
|
.ExecuteUpdateAsync(setters => setters.SetProperty(x => x.TenantId, defaultTenantId));
|
||||||
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
|
await db.BillingClients
|
||||||
.Where(r => r.Name == newRole)
|
.IgnoreQueryFilters()
|
||||||
.Select(r => (Guid?)r.Id)
|
.Where(x => x.TenantId == Guid.Empty)
|
||||||
.FirstOrDefaultAsync();
|
.ExecuteUpdateAsync(setters => setters.SetProperty(x => x.TenantId, defaultTenantId));
|
||||||
if (!newRoleId.HasValue)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var legacyUserIds = await db.UserRoles
|
await db.UserDatas
|
||||||
.Where(ur => ur.RoleId == legacyRoleId.Value)
|
.IgnoreQueryFilters()
|
||||||
.Select(ur => ur.UserId)
|
.Where(x => x.TenantId == Guid.Empty)
|
||||||
.Distinct()
|
.ExecuteUpdateAsync(setters => setters.SetProperty(x => x.TenantId, defaultTenantId));
|
||||||
.ToListAsync();
|
|
||||||
if (legacyUserIds.Count == 0)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var alreadyInNewRole = await db.UserRoles
|
await db.VigenciaLines
|
||||||
.Where(ur => ur.RoleId == newRoleId.Value && legacyUserIds.Contains(ur.UserId))
|
.IgnoreQueryFilters()
|
||||||
.Select(ur => ur.UserId)
|
.Where(x => x.TenantId == Guid.Empty)
|
||||||
.ToListAsync();
|
.ExecuteUpdateAsync(setters => setters.SetProperty(x => x.TenantId, defaultTenantId));
|
||||||
var existingSet = alreadyInNewRole.ToHashSet();
|
|
||||||
|
|
||||||
foreach (var userId in legacyUserIds)
|
await db.TrocaNumeroLines
|
||||||
{
|
.IgnoreQueryFilters()
|
||||||
if (!existingSet.Contains(userId))
|
.Where(x => x.TenantId == Guid.Empty)
|
||||||
{
|
.ExecuteUpdateAsync(setters => setters.SetProperty(x => x.TenantId, defaultTenantId));
|
||||||
db.UserRoles.Add(new IdentityUserRole<Guid>
|
|
||||||
{
|
|
||||||
UserId = userId,
|
|
||||||
RoleId = newRoleId.Value
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var legacyAssignments = await db.UserRoles
|
await db.ChipVirgemLines
|
||||||
.Where(ur => ur.RoleId == legacyRoleId.Value)
|
.IgnoreQueryFilters()
|
||||||
.ToListAsync();
|
.Where(x => x.TenantId == Guid.Empty)
|
||||||
if (legacyAssignments.Count > 0)
|
.ExecuteUpdateAsync(setters => setters.SetProperty(x => x.TenantId, defaultTenantId));
|
||||||
{
|
|
||||||
db.UserRoles.RemoveRange(legacyAssignments);
|
|
||||||
}
|
|
||||||
|
|
||||||
await db.SaveChangesAsync();
|
await db.ControleRecebidoLines
|
||||||
|
.IgnoreQueryFilters()
|
||||||
|
.Where(x => x.TenantId == Guid.Empty)
|
||||||
|
.ExecuteUpdateAsync(setters => setters.SetProperty(x => x.TenantId, defaultTenantId));
|
||||||
|
|
||||||
var legacyRoleStillUsed = await db.UserRoles.AnyAsync(ur => ur.RoleId == legacyRoleId.Value);
|
await db.Notifications
|
||||||
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.ResumoMacrophonyPlans
|
||||||
await roleManager.DeleteAsync(legacyRoleEntity);
|
.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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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; }
|
||||||
|
|
|
||||||
|
|
@ -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>();
|
|
||||||
}
|
|
||||||
|
|
@ -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; } = "";
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
$$;
|
|
||||||
""");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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;""");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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");
|
||||||
|
|
|
||||||
|
|
@ -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; }
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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; }
|
||||||
|
|
||||||
|
|
|
||||||
15
Program.cs
15
Program.cs
|
|
@ -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
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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];
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
namespace line_gestao_api.Services;
|
|
||||||
|
|
||||||
public interface ISystemAuditService
|
|
||||||
{
|
|
||||||
Task LogAsync(string action, Guid targetTenantId, object? metadata = null, CancellationToken cancellationToken = default);
|
|
||||||
}
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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";
|
|
||||||
}
|
|
||||||
|
|
@ -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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
|
|
@ -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));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.");
|
||||||
|
|
|
||||||
|
|
@ -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}";
|
||||||
|
|
|
||||||
|
|
@ -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!"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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!"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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" />
|
||||||
|
|
|
||||||
|
|
@ -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);",
|
|
||||||
" });",
|
|
||||||
"}"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
@ -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"
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue