Compare commits
8 Commits
5101c3665a
...
9d7306c395
| Author | SHA1 | Date |
|---|---|---|
|
|
9d7306c395 | |
|
|
1f255888b0 | |
|
|
024b7d299d | |
|
|
242f8bc707 | |
|
|
7a7b5db73e | |
|
|
8f0fa83b78 | |
|
|
0d51d39c5c | |
|
|
0ab7fa955f |
|
|
@ -6,6 +6,9 @@
|
||||||
# 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,4 +1,5 @@
|
||||||
using System.IdentityModel.Tokens.Jwt;
|
using System.Globalization;
|
||||||
|
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;
|
||||||
|
|
@ -22,21 +23,24 @@ 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 = "admin")]
|
[Authorize(Roles = "sysadmin")]
|
||||||
public async Task<IActionResult> Register(RegisterRequest req)
|
public async Task<IActionResult> Register(RegisterRequest req)
|
||||||
{
|
{
|
||||||
if (req.Password != req.ConfirmPassword)
|
if (req.Password != req.ConfirmPassword)
|
||||||
|
|
@ -69,12 +73,14 @@ 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, "leitura");
|
await _userManager.AddToRoleAsync(user, AppRoles.Cliente);
|
||||||
|
|
||||||
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));
|
||||||
}
|
}
|
||||||
|
|
@ -141,6 +147,8 @@ 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));
|
||||||
}
|
}
|
||||||
|
|
@ -192,23 +200,216 @@ 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 fallbackTenantId = await _db.Tenants
|
var existsAndActive = await _db.Tenants
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.OrderBy(t => t.CreatedAt)
|
.AnyAsync(t => t.Id == user.TenantId && t.Ativo);
|
||||||
.Select(t => (Guid?)t.Id)
|
|
||||||
.FirstOrDefaultAsync();
|
|
||||||
|
|
||||||
if (!fallbackTenantId.HasValue || fallbackTenantId.Value == Guid.Empty)
|
if (!existsAndActive)
|
||||||
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 = "admin")]
|
[Authorize(Roles = "sysadmin,gestor")]
|
||||||
public class BillingController : ControllerBase
|
public class BillingController : ControllerBase
|
||||||
{
|
{
|
||||||
private readonly AppDbContext _db;
|
private readonly AppDbContext _db;
|
||||||
|
|
@ -43,11 +43,20 @@ 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) ||
|
||||||
|
|
@ -188,7 +197,7 @@ namespace line_gestao_api.Controllers
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPut("{id:guid}")]
|
[HttpPut("{id:guid}")]
|
||||||
[Authorize(Roles = "admin")]
|
[Authorize(Roles = "sysadmin,gestor")]
|
||||||
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);
|
||||||
|
|
@ -221,7 +230,7 @@ namespace line_gestao_api.Controllers
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpDelete("{id:guid}")]
|
[HttpDelete("{id:guid}")]
|
||||||
[Authorize(Roles = "admin")]
|
[Authorize(Roles = "sysadmin,gestor")]
|
||||||
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 = "admin")]
|
[Authorize(Roles = "sysadmin,gestor")]
|
||||||
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 = "admin,gestor")]
|
[Authorize(Roles = "sysadmin,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 = "admin")]
|
[Authorize(Roles = "sysadmin,gestor")]
|
||||||
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 = "admin")]
|
[Authorize(Roles = "sysadmin,gestor")]
|
||||||
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 = "admin")]
|
[Authorize(Roles = "sysadmin,gestor")]
|
||||||
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 = "admin,gestor")]
|
[Authorize(Roles = "sysadmin,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 = "admin")]
|
[Authorize(Roles = "sysadmin,gestor")]
|
||||||
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 = "admin")]
|
[Authorize(Roles = "sysadmin,gestor")]
|
||||||
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 = "admin")]
|
[Authorize(Roles = "sysadmin,gestor")]
|
||||||
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] Guid? userId,
|
[FromQuery] string? user,
|
||||||
[FromQuery] string? search,
|
[FromQuery] string? search,
|
||||||
[FromQuery] DateTime? dateFrom,
|
[FromQuery] DateTime? dateFrom,
|
||||||
[FromQuery] DateTime? dateTo,
|
[FromQuery] DateTime? dateTo,
|
||||||
|
|
@ -60,15 +60,17 @@ 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 (userId.HasValue)
|
if (!string.IsNullOrWhiteSpace(user))
|
||||||
{
|
{
|
||||||
q = q.Where(x => x.UserId == userId.Value);
|
var u = user.Trim();
|
||||||
|
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 =>
|
||||||
|
|
@ -83,7 +85,6 @@ 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 = "admin,gestor")]
|
[Authorize(Roles = "sysadmin,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 = "admin,gestor")]
|
[Authorize(Roles = "sysadmin,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 = "admin")]
|
[Authorize(Roles = "sysadmin")]
|
||||||
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 = "admin,gestor")]
|
[Authorize(Roles = "sysadmin,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,7 +45,9 @@ 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 = "admin")]
|
[Authorize(Roles = "sysadmin,gestor")]
|
||||||
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 = "admin,gestor")]
|
[Authorize(Roles = "sysadmin,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 = "admin,gestor")]
|
[Authorize(Roles = "sysadmin,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 = "admin")]
|
[Authorize(Roles = "sysadmin,gestor")]
|
||||||
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,6 +32,7 @@ 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();
|
||||||
|
|
||||||
|
|
@ -44,27 +45,35 @@ 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 qLines.CountAsync(x =>
|
var bloqueadosPerdaRoubo = await qLinesWithClient.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 qLines.CountAsync(x =>
|
var bloqueados120Dias = await qLinesWithClient.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 qLines.CountAsync(x =>
|
var bloqueadosOutros = await qLinesWithClient.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%"))
|
||||||
);
|
);
|
||||||
|
|
||||||
var bloqueados = bloqueadosPerdaRoubo + bloqueados120Dias + bloqueadosOutros;
|
// Regra do KPI "Bloqueadas" alinhada ao critério da página Geral:
|
||||||
|
// 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.Cliente ?? "").Trim(), "%RESERVA%") ||
|
EF.Functions.ILike((x.Usuario ?? "").Trim(), "RESERVA") ||
|
||||||
EF.Functions.ILike((x.Usuario ?? "").Trim(), "%RESERVA%") ||
|
EF.Functions.ILike((x.Skil ?? "").Trim(), "RESERVA") ||
|
||||||
EF.Functions.ILike((x.Skil ?? "").Trim(), "%RESERVA%"));
|
EF.Functions.ILike((x.Cliente ?? "").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,3 +1,5 @@
|
||||||
|
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;
|
||||||
|
|
@ -12,6 +14,8 @@ 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;
|
||||||
|
|
||||||
|
|
@ -23,6 +27,36 @@ 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)
|
||||||
|
|
@ -177,6 +211,302 @@ public class ResumoController : ControllerBase
|
||||||
response.VivoLineTotals ??= new ResumoVivoLineTotalDto();
|
response.VivoLineTotals ??= new ResumoVivoLineTotalDto();
|
||||||
response.VivoLineTotals.QtdLinhasTotal = canonicalTotalLinhas;
|
response.VivoLineTotals.QtdLinhasTotal = canonicalTotalLinhas;
|
||||||
|
|
||||||
return Ok(response);
|
return 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,424 @@
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
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 = "admin,gestor")]
|
[Authorize(Roles = "sysadmin,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 = "admin,gestor")]
|
[Authorize(Roles = "sysadmin,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 = "admin")]
|
[Authorize(Roles = "sysadmin")]
|
||||||
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,6 +56,7 @@ 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}%") ||
|
||||||
|
|
@ -151,6 +152,7 @@ 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}%") ||
|
||||||
|
|
@ -261,7 +263,7 @@ namespace line_gestao_api.Controllers
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
[Authorize(Roles = "admin")]
|
[Authorize(Roles = "sysadmin")]
|
||||||
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;
|
||||||
|
|
@ -363,7 +365,7 @@ namespace line_gestao_api.Controllers
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPut("{id:guid}")]
|
[HttpPut("{id:guid}")]
|
||||||
[Authorize(Roles = "admin")]
|
[Authorize(Roles = "sysadmin")]
|
||||||
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);
|
||||||
|
|
@ -395,7 +397,7 @@ namespace line_gestao_api.Controllers
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpDelete("{id:guid}")]
|
[HttpDelete("{id:guid}")]
|
||||||
[Authorize(Roles = "admin")]
|
[Authorize(Roles = "sysadmin")]
|
||||||
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,8 +17,9 @@ public class UsersController : ControllerBase
|
||||||
{
|
{
|
||||||
private static readonly HashSet<string> AllowedRoles = new(StringComparer.OrdinalIgnoreCase)
|
private static readonly HashSet<string> AllowedRoles = new(StringComparer.OrdinalIgnoreCase)
|
||||||
{
|
{
|
||||||
"admin",
|
AppRoles.SysAdmin,
|
||||||
"gestor"
|
AppRoles.Gestor,
|
||||||
|
AppRoles.Cliente
|
||||||
};
|
};
|
||||||
|
|
||||||
private readonly AppDbContext _db;
|
private readonly AppDbContext _db;
|
||||||
|
|
@ -39,7 +40,7 @@ public class UsersController : ControllerBase
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
[Authorize(Roles = "admin")]
|
[Authorize(Roles = "sysadmin")]
|
||||||
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);
|
||||||
|
|
@ -122,7 +123,7 @@ public class UsersController : ControllerBase
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
[Authorize(Roles = "admin")]
|
[Authorize(Roles = "sysadmin")]
|
||||||
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,
|
||||||
|
|
@ -132,7 +133,9 @@ 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.AsNoTracking();
|
var usersQuery = _userManager.Users
|
||||||
|
.IgnoreQueryFilters()
|
||||||
|
.AsNoTracking();
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(search))
|
if (!string.IsNullOrWhiteSpace(search))
|
||||||
{
|
{
|
||||||
|
|
@ -191,10 +194,13 @@ public class UsersController : ControllerBase
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{id:guid}")]
|
[HttpGet("{id:guid}")]
|
||||||
[Authorize(Roles = "admin")]
|
[Authorize(Roles = "sysadmin")]
|
||||||
public async Task<ActionResult<UserListItemDto>> GetById(Guid id)
|
public async Task<ActionResult<UserListItemDto>> GetById(Guid id)
|
||||||
{
|
{
|
||||||
var user = await _userManager.Users.AsNoTracking().FirstOrDefaultAsync(u => u.Id == id);
|
var user = await _userManager.Users
|
||||||
|
.IgnoreQueryFilters()
|
||||||
|
.AsNoTracking()
|
||||||
|
.FirstOrDefaultAsync(u => u.Id == id);
|
||||||
if (user == null)
|
if (user == null)
|
||||||
{
|
{
|
||||||
return NotFound();
|
return NotFound();
|
||||||
|
|
@ -215,7 +221,7 @@ public class UsersController : ControllerBase
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPatch("{id:guid}")]
|
[HttpPatch("{id:guid}")]
|
||||||
[Authorize(Roles = "admin")]
|
[Authorize(Roles = "sysadmin")]
|
||||||
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);
|
||||||
|
|
@ -224,7 +230,9 @@ public class UsersController : ControllerBase
|
||||||
return BadRequest(new ValidationErrorResponse { Errors = errors });
|
return BadRequest(new ValidationErrorResponse { Errors = errors });
|
||||||
}
|
}
|
||||||
|
|
||||||
var user = await _userManager.Users.FirstOrDefaultAsync(u => u.Id == id);
|
var user = await _userManager.Users
|
||||||
|
.IgnoreQueryFilters()
|
||||||
|
.FirstOrDefaultAsync(u => u.Id == id);
|
||||||
if (user == null)
|
if (user == null)
|
||||||
{
|
{
|
||||||
return NotFound();
|
return NotFound();
|
||||||
|
|
@ -295,14 +303,9 @@ public class UsersController : ControllerBase
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpDelete("{id:guid}")]
|
[HttpDelete("{id:guid}")]
|
||||||
[Authorize(Roles = "admin")]
|
[Authorize(Roles = "sysadmin")]
|
||||||
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)
|
||||||
{
|
{
|
||||||
|
|
@ -315,12 +318,14 @@ public class UsersController : ControllerBase
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
var tenantId = _tenantProvider.TenantId.Value;
|
var user = await _userManager.Users
|
||||||
var user = await _userManager.Users.FirstOrDefaultAsync(u => u.Id == id && u.TenantId == tenantId);
|
.IgnoreQueryFilters()
|
||||||
|
.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)
|
||||||
{
|
{
|
||||||
|
|
@ -334,12 +339,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, "admin", StringComparison.OrdinalIgnoreCase));
|
var isAdmin = targetRoles.Any(r => string.Equals(r, AppRoles.SysAdmin, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
if (isAdmin)
|
if (isAdmin)
|
||||||
{
|
{
|
||||||
var adminRoleId = await _roleManager.Roles
|
var adminRoleId = await _roleManager.Roles
|
||||||
.Where(r => r.Name == "admin")
|
.Where(r => r.Name == AppRoles.SysAdmin)
|
||||||
.Select(r => (Guid?)r.Id)
|
.Select(r => (Guid?)r.Id)
|
||||||
.FirstOrDefaultAsync();
|
.FirstOrDefaultAsync();
|
||||||
|
|
||||||
|
|
@ -360,7 +365,7 @@ public class UsersController : ControllerBase
|
||||||
{
|
{
|
||||||
Errors = new List<ValidationErrorDto>
|
Errors = new List<ValidationErrorDto>
|
||||||
{
|
{
|
||||||
new() { Field = "usuario", Message = "Não é permitido excluir o último administrador." }
|
new() { Field = "usuario", Message = "Não é permitido excluir o último sysadmin." }
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -422,6 +427,10 @@ 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)
|
||||||
{
|
{
|
||||||
|
|
@ -433,22 +442,22 @@ public class UsersController : ControllerBase
|
||||||
var email = req.Email.Trim().ToLowerInvariant();
|
var email = req.Email.Trim().ToLowerInvariant();
|
||||||
var normalized = _userManager.NormalizeEmail(email);
|
var normalized = _userManager.NormalizeEmail(email);
|
||||||
|
|
||||||
var tenantId = _tenantProvider.TenantId;
|
if (targetUser == null)
|
||||||
if (tenantId == null)
|
|
||||||
{
|
{
|
||||||
errors.Add(new ValidationErrorDto { Field = "email", Message = "Tenant inválido." });
|
return errors;
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
var exists = await _userManager.Users.AnyAsync(u =>
|
|
||||||
u.Id != userId &&
|
|
||||||
u.TenantId == tenantId &&
|
|
||||||
u.NormalizedEmail == normalized);
|
|
||||||
|
|
||||||
if (exists)
|
var tenantId = targetUser.TenantId;
|
||||||
{
|
var exists = await _userManager.Users
|
||||||
errors.Add(new ValidationErrorDto { Field = "email", Message = "E-mail já cadastrado." });
|
.IgnoreQueryFilters()
|
||||||
}
|
.AnyAsync(u =>
|
||||||
|
u.Id != userId &&
|
||||||
|
u.TenantId == tenantId &&
|
||||||
|
u.NormalizedEmail == normalized);
|
||||||
|
|
||||||
|
if (exists)
|
||||||
|
{
|
||||||
|
errors.Add(new ValidationErrorDto { Field = "email", Message = "E-mail já cadastrado." });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,7 @@ 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}%") ||
|
||||||
|
|
@ -98,6 +99,10 @@ 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();
|
||||||
|
|
@ -142,6 +147,7 @@ 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}%") ||
|
||||||
|
|
@ -239,6 +245,10 @@ 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
|
||||||
|
|
@ -246,7 +256,7 @@ namespace line_gestao_api.Controllers
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
[Authorize(Roles = "admin")]
|
[Authorize(Roles = "sysadmin")]
|
||||||
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;
|
||||||
|
|
@ -333,6 +343,10 @@ 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
|
||||||
|
|
@ -340,12 +354,15 @@ namespace line_gestao_api.Controllers
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPut("{id:guid}")]
|
[HttpPut("{id:guid}")]
|
||||||
[Authorize(Roles = "admin")]
|
[Authorize(Roles = "sysadmin")]
|
||||||
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);
|
||||||
|
|
@ -358,6 +375,13 @@ 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();
|
||||||
|
|
@ -365,8 +389,41 @@ 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 = "admin")]
|
[Authorize(Roles = "sysadmin")]
|
||||||
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);
|
||||||
|
|
@ -390,6 +447,20 @@ 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,6 +78,15 @@ 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)
|
||||||
// =========================
|
// =========================
|
||||||
|
|
@ -170,6 +179,7 @@ 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);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -270,6 +280,7 @@ 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);
|
||||||
|
|
@ -281,8 +292,13 @@ 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);
|
||||||
|
|
@ -318,33 +334,33 @@ public class AppDbContext : IdentityDbContext<ApplicationUser, IdentityRole<Guid
|
||||||
.OnDelete(DeleteBehavior.Cascade);
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity<MobileLine>().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId);
|
modelBuilder.Entity<MobileLine>().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<MuregLine>().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<BillingClient>().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<UserData>().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<VigenciaLine>().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<TrocaNumeroLine>().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<ChipVirgemLine>().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<ControleRecebidoLine>().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<Notification>().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<ResumoMacrophonyPlan>().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<ResumoMacrophonyTotal>().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<ResumoVivoLineResumo>().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<ResumoVivoLineTotal>().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<ResumoClienteEspecial>().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<ResumoPlanoContratoResumo>().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<ResumoPlanoContratoTotal>().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<ResumoLineTotais>().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<ResumoGbDistribuicao>().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<ResumoGbDistribuicaoTotal>().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<ResumoReservaLine>().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<ResumoReservaTotal>().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<ParcelamentoLine>().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<ParcelamentoMonthValue>().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<AuditLog>().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<ImportAuditRun>().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<ImportAuditIssue>().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId));
|
||||||
modelBuilder.Entity<ApplicationUser>().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId);
|
modelBuilder.Entity<ApplicationUser>().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId));
|
||||||
}
|
}
|
||||||
|
|
||||||
public override int SaveChanges()
|
public override int SaveChanges()
|
||||||
|
|
@ -363,12 +379,12 @@ public class AppDbContext : IdentityDbContext<ApplicationUser, IdentityRole<Guid
|
||||||
|
|
||||||
private void ApplyTenantIds()
|
private void ApplyTenantIds()
|
||||||
{
|
{
|
||||||
if (_tenantProvider.TenantId == null)
|
if (_tenantProvider.ActorTenantId == null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var tenantId = _tenantProvider.TenantId.Value;
|
var tenantId = _tenantProvider.ActorTenantId.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)
|
||||||
|
|
|
||||||
385
Data/SeedData.cs
385
Data/SeedData.cs
|
|
@ -9,17 +9,14 @@ 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 DefaultTenantName { get; set; } = "Default";
|
public string AdminMasterName { get; set; } = "System Admin";
|
||||||
public string AdminName { get; set; } = "Administrador";
|
public string? AdminMasterEmail { get; set; } = "sysadmin@linegestao.local";
|
||||||
public string AdminEmail { get; set; } = "admin@linegestao.local";
|
public string? AdminMasterPassword { get; set; } = "DevSysAdmin123!";
|
||||||
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();
|
||||||
|
|
@ -29,224 +26,236 @@ 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;
|
||||||
|
|
||||||
await db.Database.MigrateAsync();
|
if (db.Database.IsRelational())
|
||||||
|
{
|
||||||
|
await db.Database.MigrateAsync();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await db.Database.EnsureCreatedAsync();
|
||||||
|
}
|
||||||
|
|
||||||
if (!options.Enabled)
|
if (!options.Enabled)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var roles = new[] { "admin", "gestor", "operador", "leitura" };
|
var systemTenantId = SystemTenantConstants.SystemTenantId;
|
||||||
|
var roles = AppRoles.All;
|
||||||
foreach (var role in roles)
|
foreach (var role in roles)
|
||||||
{
|
{
|
||||||
if (!await roleManager.RoleExistsAsync(role))
|
if (!await roleManager.RoleExistsAsync(role))
|
||||||
{
|
{
|
||||||
await roleManager.CreateAsync(new IdentityRole<Guid>(role));
|
var roleResult = await roleManager.CreateAsync(new IdentityRole<Guid>(role));
|
||||||
|
EnsureIdentitySucceeded(roleResult, $"Falha ao criar role '{role}'.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var tenant = await db.Tenants.FirstOrDefaultAsync(t => t.Id == DefaultTenantId);
|
await MigrateLegacyRolesAsync(db, roleManager);
|
||||||
if (tenant == null)
|
|
||||||
|
var systemTenant = await db.Tenants.FirstOrDefaultAsync(t => t.Id == systemTenantId);
|
||||||
|
if (systemTenant == null)
|
||||||
{
|
{
|
||||||
tenant = new Tenant
|
systemTenant = new Tenant
|
||||||
{
|
{
|
||||||
Id = DefaultTenantId,
|
Id = systemTenantId,
|
||||||
Name = options.DefaultTenantName,
|
NomeOficial = SystemTenantConstants.SystemTenantNomeOficial,
|
||||||
|
IsSystem = true,
|
||||||
|
Ativo = true,
|
||||||
CreatedAt = DateTime.UtcNow
|
CreatedAt = DateTime.UtcNow
|
||||||
};
|
};
|
||||||
|
db.Tenants.Add(systemTenant);
|
||||||
db.Tenants.Add(tenant);
|
}
|
||||||
await db.SaveChangesAsync();
|
else
|
||||||
|
{
|
||||||
|
systemTenant.NomeOficial = SystemTenantConstants.SystemTenantNomeOficial;
|
||||||
|
systemTenant.IsSystem = true;
|
||||||
|
systemTenant.Ativo = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
await NormalizeLegacyTenantDataAsync(db, tenant.Id);
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
tenantProvider.SetTenantId(tenant.Id);
|
var emailFromEnv = Environment.GetEnvironmentVariable("SYSADMIN_EMAIL")
|
||||||
|
?? Environment.GetEnvironmentVariable("ADMIN_MASTER_EMAIL");
|
||||||
|
var passwordFromEnv = Environment.GetEnvironmentVariable("SYSADMIN_PASSWORD")
|
||||||
|
?? Environment.GetEnvironmentVariable("ADMIN_MASTER_PASSWORD");
|
||||||
|
|
||||||
var normalizedEmail = userManager.NormalizeEmail(options.AdminEmail);
|
var adminMasterEmail = (emailFromEnv ?? options.AdminMasterEmail ?? string.Empty).Trim().ToLowerInvariant();
|
||||||
var existingAdmin = await userManager.Users
|
var adminMasterPassword = passwordFromEnv ?? options.AdminMasterPassword ?? string.Empty;
|
||||||
.FirstOrDefaultAsync(u => u.NormalizedEmail == normalizedEmail && u.TenantId == tenant.Id);
|
|
||||||
|
|
||||||
if (existingAdmin == null)
|
if (string.IsNullOrWhiteSpace(adminMasterEmail) || string.IsNullOrWhiteSpace(adminMasterPassword))
|
||||||
{
|
{
|
||||||
var adminUser = new ApplicationUser
|
throw new InvalidOperationException(
|
||||||
{
|
"Credenciais do sysadmin ausentes. Defina SYSADMIN_EMAIL e SYSADMIN_PASSWORD (ou Seed:AdminMasterEmail/Seed:AdminMasterPassword).");
|
||||||
UserName = options.AdminEmail,
|
|
||||||
Email = options.AdminEmail,
|
|
||||||
Name = options.AdminName,
|
|
||||||
TenantId = tenant.Id,
|
|
||||||
EmailConfirmed = true,
|
|
||||||
IsActive = true,
|
|
||||||
LockoutEnabled = true
|
|
||||||
};
|
|
||||||
|
|
||||||
var createResult = await userManager.CreateAsync(adminUser, options.AdminPassword);
|
|
||||||
if (createResult.Succeeded)
|
|
||||||
{
|
|
||||||
await userManager.AddToRoleAsync(adminUser, "admin");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else if (options.ReapplyAdminCredentialsOnStartup)
|
|
||||||
|
var normalizedEmail = userManager.NormalizeEmail(adminMasterEmail);
|
||||||
|
|
||||||
|
var previousTenant = tenantProvider.TenantId;
|
||||||
|
tenantProvider.SetTenantId(systemTenantId);
|
||||||
|
|
||||||
|
try
|
||||||
{
|
{
|
||||||
existingAdmin.Name = options.AdminName;
|
var existingAdminMaster = await userManager.Users
|
||||||
existingAdmin.Email = options.AdminEmail;
|
.IgnoreQueryFilters()
|
||||||
existingAdmin.UserName = options.AdminEmail;
|
.FirstOrDefaultAsync(u => u.TenantId == systemTenantId && u.NormalizedEmail == normalizedEmail);
|
||||||
existingAdmin.EmailConfirmed = true;
|
|
||||||
existingAdmin.IsActive = true;
|
|
||||||
existingAdmin.LockoutEnabled = true;
|
|
||||||
|
|
||||||
await userManager.SetLockoutEndDateAsync(existingAdmin, null);
|
if (existingAdminMaster == null)
|
||||||
await userManager.ResetAccessFailedCountAsync(existingAdmin);
|
|
||||||
await userManager.UpdateAsync(existingAdmin);
|
|
||||||
|
|
||||||
var resetToken = await userManager.GeneratePasswordResetTokenAsync(existingAdmin);
|
|
||||||
var resetPasswordResult = await userManager.ResetPasswordAsync(existingAdmin, resetToken, options.AdminPassword);
|
|
||||||
if (!resetPasswordResult.Succeeded)
|
|
||||||
{
|
{
|
||||||
var removePasswordResult = await userManager.RemovePasswordAsync(existingAdmin);
|
var adminMaster = new ApplicationUser
|
||||||
if (removePasswordResult.Succeeded)
|
|
||||||
{
|
{
|
||||||
await userManager.AddPasswordAsync(existingAdmin, options.AdminPassword);
|
Name = options.AdminMasterName,
|
||||||
|
Email = adminMasterEmail,
|
||||||
|
UserName = adminMasterEmail,
|
||||||
|
TenantId = systemTenantId,
|
||||||
|
EmailConfirmed = true,
|
||||||
|
IsActive = true,
|
||||||
|
LockoutEnabled = true
|
||||||
|
};
|
||||||
|
|
||||||
|
var createResult = await userManager.CreateAsync(adminMaster, adminMasterPassword);
|
||||||
|
EnsureIdentitySucceeded(createResult, "Falha ao criar usuário sysadmin.");
|
||||||
|
|
||||||
|
var addRoleResult = await userManager.AddToRoleAsync(adminMaster, SystemTenantConstants.SystemRole);
|
||||||
|
EnsureIdentitySucceeded(addRoleResult, "Falha ao associar role sysadmin ao usuário inicial.");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
existingAdminMaster.Name = options.AdminMasterName;
|
||||||
|
existingAdminMaster.Email = adminMasterEmail;
|
||||||
|
existingAdminMaster.UserName = adminMasterEmail;
|
||||||
|
existingAdminMaster.EmailConfirmed = true;
|
||||||
|
existingAdminMaster.IsActive = true;
|
||||||
|
existingAdminMaster.LockoutEnabled = true;
|
||||||
|
|
||||||
|
var updateResult = await userManager.UpdateAsync(existingAdminMaster);
|
||||||
|
EnsureIdentitySucceeded(updateResult, "Falha ao atualizar usuário sysadmin.");
|
||||||
|
|
||||||
|
if (options.ReapplyAdminCredentialsOnStartup)
|
||||||
|
{
|
||||||
|
await userManager.SetLockoutEndDateAsync(existingAdminMaster, null);
|
||||||
|
await userManager.ResetAccessFailedCountAsync(existingAdminMaster);
|
||||||
|
|
||||||
|
var resetToken = await userManager.GeneratePasswordResetTokenAsync(existingAdminMaster);
|
||||||
|
var resetPasswordResult = await userManager.ResetPasswordAsync(existingAdminMaster, resetToken, adminMasterPassword);
|
||||||
|
if (!resetPasswordResult.Succeeded)
|
||||||
|
{
|
||||||
|
var removePasswordResult = await userManager.RemovePasswordAsync(existingAdminMaster);
|
||||||
|
if (removePasswordResult.Succeeded)
|
||||||
|
{
|
||||||
|
var addPasswordResult = await userManager.AddPasswordAsync(existingAdminMaster, adminMasterPassword);
|
||||||
|
EnsureIdentitySucceeded(addPasswordResult, "Falha ao definir senha do sysadmin.");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var addPasswordResult = await userManager.AddPasswordAsync(existingAdminMaster, adminMasterPassword);
|
||||||
|
EnsureIdentitySucceeded(addPasswordResult, "Falha ao definir senha do sysadmin.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!await userManager.IsInRoleAsync(existingAdminMaster, SystemTenantConstants.SystemRole))
|
||||||
|
{
|
||||||
|
var addRoleResult = await userManager.AddToRoleAsync(existingAdminMaster, SystemTenantConstants.SystemRole);
|
||||||
|
EnsureIdentitySucceeded(addRoleResult, "Falha ao associar role sysadmin ao usuário inicial.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
tenantProvider.SetTenantId(previousTenant);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!await userManager.IsInRoleAsync(existingAdmin, "admin"))
|
private static void EnsureIdentitySucceeded(IdentityResult result, string message)
|
||||||
|
{
|
||||||
|
if (result.Succeeded)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var details = string.Join("; ", result.Errors.Select(e => e.Description));
|
||||||
|
throw new InvalidOperationException($"{message} Detalhes: {details}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task MigrateLegacyRolesAsync(AppDbContext db, RoleManager<IdentityRole<Guid>> roleManager)
|
||||||
|
{
|
||||||
|
await MigrateLegacyRoleAsync(db, roleManager, "admin_master", AppRoles.SysAdmin);
|
||||||
|
await MigrateLegacyRoleAsync(db, roleManager, "admin", AppRoles.SysAdmin);
|
||||||
|
await MigrateLegacyRoleAsync(db, roleManager, "leitura", AppRoles.Cliente);
|
||||||
|
await MigrateLegacyRoleAsync(db, roleManager, "operador", AppRoles.Cliente);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task MigrateLegacyRoleAsync(
|
||||||
|
AppDbContext db,
|
||||||
|
RoleManager<IdentityRole<Guid>> roleManager,
|
||||||
|
string legacyRole,
|
||||||
|
string newRole)
|
||||||
|
{
|
||||||
|
var legacyRoleId = await roleManager.Roles
|
||||||
|
.Where(r => r.Name == legacyRole)
|
||||||
|
.Select(r => (Guid?)r.Id)
|
||||||
|
.FirstOrDefaultAsync();
|
||||||
|
if (!legacyRoleId.HasValue)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var newRoleId = await roleManager.Roles
|
||||||
|
.Where(r => r.Name == newRole)
|
||||||
|
.Select(r => (Guid?)r.Id)
|
||||||
|
.FirstOrDefaultAsync();
|
||||||
|
if (!newRoleId.HasValue)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var legacyUserIds = await db.UserRoles
|
||||||
|
.Where(ur => ur.RoleId == legacyRoleId.Value)
|
||||||
|
.Select(ur => ur.UserId)
|
||||||
|
.Distinct()
|
||||||
|
.ToListAsync();
|
||||||
|
if (legacyUserIds.Count == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var alreadyInNewRole = await db.UserRoles
|
||||||
|
.Where(ur => ur.RoleId == newRoleId.Value && legacyUserIds.Contains(ur.UserId))
|
||||||
|
.Select(ur => ur.UserId)
|
||||||
|
.ToListAsync();
|
||||||
|
var existingSet = alreadyInNewRole.ToHashSet();
|
||||||
|
|
||||||
|
foreach (var userId in legacyUserIds)
|
||||||
|
{
|
||||||
|
if (!existingSet.Contains(userId))
|
||||||
{
|
{
|
||||||
await userManager.AddToRoleAsync(existingAdmin, "admin");
|
db.UserRoles.Add(new IdentityUserRole<Guid>
|
||||||
|
{
|
||||||
|
UserId = userId,
|
||||||
|
RoleId = newRoleId.Value
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tenantProvider.SetTenantId(null);
|
var legacyAssignments = await db.UserRoles
|
||||||
}
|
.Where(ur => ur.RoleId == legacyRoleId.Value)
|
||||||
|
.ToListAsync();
|
||||||
|
if (legacyAssignments.Count > 0)
|
||||||
|
{
|
||||||
|
db.UserRoles.RemoveRange(legacyAssignments);
|
||||||
|
}
|
||||||
|
|
||||||
private static async Task NormalizeLegacyTenantDataAsync(AppDbContext db, Guid defaultTenantId)
|
await db.SaveChangesAsync();
|
||||||
{
|
|
||||||
if (defaultTenantId == Guid.Empty)
|
|
||||||
return;
|
|
||||||
|
|
||||||
await db.Users
|
var legacyRoleStillUsed = await db.UserRoles.AnyAsync(ur => ur.RoleId == legacyRoleId.Value);
|
||||||
.IgnoreQueryFilters()
|
if (!legacyRoleStillUsed)
|
||||||
.Where(x => x.TenantId == Guid.Empty)
|
{
|
||||||
.ExecuteUpdateAsync(setters => setters.SetProperty(x => x.TenantId, defaultTenantId));
|
var legacyRoleEntity = await roleManager.Roles.FirstOrDefaultAsync(r => r.Id == legacyRoleId.Value);
|
||||||
|
if (legacyRoleEntity != null)
|
||||||
await db.MobileLines
|
{
|
||||||
.IgnoreQueryFilters()
|
await roleManager.DeleteAsync(legacyRoleEntity);
|
||||||
.Where(x => x.TenantId == Guid.Empty)
|
}
|
||||||
.ExecuteUpdateAsync(setters => setters.SetProperty(x => x.TenantId, defaultTenantId));
|
}
|
||||||
|
|
||||||
await db.MuregLines
|
|
||||||
.IgnoreQueryFilters()
|
|
||||||
.Where(x => x.TenantId == Guid.Empty)
|
|
||||||
.ExecuteUpdateAsync(setters => setters.SetProperty(x => x.TenantId, defaultTenantId));
|
|
||||||
|
|
||||||
await db.BillingClients
|
|
||||||
.IgnoreQueryFilters()
|
|
||||||
.Where(x => x.TenantId == Guid.Empty)
|
|
||||||
.ExecuteUpdateAsync(setters => setters.SetProperty(x => x.TenantId, defaultTenantId));
|
|
||||||
|
|
||||||
await db.UserDatas
|
|
||||||
.IgnoreQueryFilters()
|
|
||||||
.Where(x => x.TenantId == Guid.Empty)
|
|
||||||
.ExecuteUpdateAsync(setters => setters.SetProperty(x => x.TenantId, defaultTenantId));
|
|
||||||
|
|
||||||
await db.VigenciaLines
|
|
||||||
.IgnoreQueryFilters()
|
|
||||||
.Where(x => x.TenantId == Guid.Empty)
|
|
||||||
.ExecuteUpdateAsync(setters => setters.SetProperty(x => x.TenantId, defaultTenantId));
|
|
||||||
|
|
||||||
await db.TrocaNumeroLines
|
|
||||||
.IgnoreQueryFilters()
|
|
||||||
.Where(x => x.TenantId == Guid.Empty)
|
|
||||||
.ExecuteUpdateAsync(setters => setters.SetProperty(x => x.TenantId, defaultTenantId));
|
|
||||||
|
|
||||||
await db.ChipVirgemLines
|
|
||||||
.IgnoreQueryFilters()
|
|
||||||
.Where(x => x.TenantId == Guid.Empty)
|
|
||||||
.ExecuteUpdateAsync(setters => setters.SetProperty(x => x.TenantId, defaultTenantId));
|
|
||||||
|
|
||||||
await db.ControleRecebidoLines
|
|
||||||
.IgnoreQueryFilters()
|
|
||||||
.Where(x => x.TenantId == Guid.Empty)
|
|
||||||
.ExecuteUpdateAsync(setters => setters.SetProperty(x => x.TenantId, defaultTenantId));
|
|
||||||
|
|
||||||
await db.Notifications
|
|
||||||
.IgnoreQueryFilters()
|
|
||||||
.Where(x => x.TenantId == Guid.Empty)
|
|
||||||
.ExecuteUpdateAsync(setters => setters.SetProperty(x => x.TenantId, defaultTenantId));
|
|
||||||
|
|
||||||
await db.ResumoMacrophonyPlans
|
|
||||||
.IgnoreQueryFilters()
|
|
||||||
.Where(x => x.TenantId == Guid.Empty)
|
|
||||||
.ExecuteUpdateAsync(setters => setters.SetProperty(x => x.TenantId, defaultTenantId));
|
|
||||||
|
|
||||||
await db.ResumoMacrophonyTotals
|
|
||||||
.IgnoreQueryFilters()
|
|
||||||
.Where(x => x.TenantId == Guid.Empty)
|
|
||||||
.ExecuteUpdateAsync(setters => setters.SetProperty(x => x.TenantId, defaultTenantId));
|
|
||||||
|
|
||||||
await db.ResumoVivoLineResumos
|
|
||||||
.IgnoreQueryFilters()
|
|
||||||
.Where(x => x.TenantId == Guid.Empty)
|
|
||||||
.ExecuteUpdateAsync(setters => setters.SetProperty(x => x.TenantId, defaultTenantId));
|
|
||||||
|
|
||||||
await db.ResumoVivoLineTotals
|
|
||||||
.IgnoreQueryFilters()
|
|
||||||
.Where(x => x.TenantId == Guid.Empty)
|
|
||||||
.ExecuteUpdateAsync(setters => setters.SetProperty(x => x.TenantId, defaultTenantId));
|
|
||||||
|
|
||||||
await db.ResumoClienteEspeciais
|
|
||||||
.IgnoreQueryFilters()
|
|
||||||
.Where(x => x.TenantId == Guid.Empty)
|
|
||||||
.ExecuteUpdateAsync(setters => setters.SetProperty(x => x.TenantId, defaultTenantId));
|
|
||||||
|
|
||||||
await db.ResumoPlanoContratoResumos
|
|
||||||
.IgnoreQueryFilters()
|
|
||||||
.Where(x => x.TenantId == Guid.Empty)
|
|
||||||
.ExecuteUpdateAsync(setters => setters.SetProperty(x => x.TenantId, defaultTenantId));
|
|
||||||
|
|
||||||
await db.ResumoPlanoContratoTotals
|
|
||||||
.IgnoreQueryFilters()
|
|
||||||
.Where(x => x.TenantId == Guid.Empty)
|
|
||||||
.ExecuteUpdateAsync(setters => setters.SetProperty(x => x.TenantId, defaultTenantId));
|
|
||||||
|
|
||||||
await db.ResumoLineTotais
|
|
||||||
.IgnoreQueryFilters()
|
|
||||||
.Where(x => x.TenantId == Guid.Empty)
|
|
||||||
.ExecuteUpdateAsync(setters => setters.SetProperty(x => x.TenantId, defaultTenantId));
|
|
||||||
|
|
||||||
await db.ResumoReservaLines
|
|
||||||
.IgnoreQueryFilters()
|
|
||||||
.Where(x => x.TenantId == Guid.Empty)
|
|
||||||
.ExecuteUpdateAsync(setters => setters.SetProperty(x => x.TenantId, defaultTenantId));
|
|
||||||
|
|
||||||
await db.ResumoReservaTotals
|
|
||||||
.IgnoreQueryFilters()
|
|
||||||
.Where(x => x.TenantId == Guid.Empty)
|
|
||||||
.ExecuteUpdateAsync(setters => setters.SetProperty(x => x.TenantId, defaultTenantId));
|
|
||||||
|
|
||||||
await db.ParcelamentoLines
|
|
||||||
.IgnoreQueryFilters()
|
|
||||||
.Where(x => x.TenantId == Guid.Empty)
|
|
||||||
.ExecuteUpdateAsync(setters => setters.SetProperty(x => x.TenantId, defaultTenantId));
|
|
||||||
|
|
||||||
await db.ParcelamentoMonthValues
|
|
||||||
.IgnoreQueryFilters()
|
|
||||||
.Where(x => x.TenantId == Guid.Empty)
|
|
||||||
.ExecuteUpdateAsync(setters => setters.SetProperty(x => x.TenantId, defaultTenantId));
|
|
||||||
|
|
||||||
await db.AuditLogs
|
|
||||||
.IgnoreQueryFilters()
|
|
||||||
.Where(x => x.TenantId == Guid.Empty)
|
|
||||||
.ExecuteUpdateAsync(setters => setters.SetProperty(x => x.TenantId, defaultTenantId));
|
|
||||||
|
|
||||||
await db.ImportAuditRuns
|
|
||||||
.IgnoreQueryFilters()
|
|
||||||
.Where(x => x.TenantId == Guid.Empty)
|
|
||||||
.ExecuteUpdateAsync(setters => setters.SetProperty(x => x.TenantId, defaultTenantId));
|
|
||||||
|
|
||||||
await db.ImportAuditIssues
|
|
||||||
.IgnoreQueryFilters()
|
|
||||||
.Where(x => x.TenantId == Guid.Empty)
|
|
||||||
.ExecuteUpdateAsync(setters => setters.SetProperty(x => x.TenantId, defaultTenantId));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,74 @@
|
||||||
|
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,6 +14,7 @@
|
||||||
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; }
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
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,6 +14,10 @@ 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; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -49,6 +53,11 @@ 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; } = "";
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,110 @@
|
||||||
|
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
|
||||||
|
$$;
|
||||||
|
""");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
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.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,74 @@
|
||||||
|
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.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
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.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
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;""");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
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,6 +245,12 @@ 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");
|
||||||
|
|
@ -274,6 +280,10 @@ 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)");
|
||||||
|
|
@ -285,6 +295,9 @@ 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)");
|
||||||
|
|
@ -298,6 +311,10 @@ 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");
|
||||||
|
|
@ -306,6 +323,8 @@ 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");
|
||||||
|
|
@ -1357,15 +1376,35 @@ 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<string>("Name")
|
b.Property<bool>("IsSystem")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<string>("NomeOficial")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("text");
|
.HasMaxLength(200)
|
||||||
|
.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");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -1510,6 +1549,15 @@ 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");
|
||||||
|
|
||||||
|
|
@ -1528,6 +1576,9 @@ 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");
|
||||||
|
|
||||||
|
|
@ -1550,6 +1601,8 @@ 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,10 +6,16 @@ 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; }
|
||||||
|
|
@ -21,6 +27,7 @@ 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,6 +3,10 @@ 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 Name { get; set; } = string.Empty;
|
public string NomeOficial { 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,6 +20,10 @@ 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,4 +1,5 @@
|
||||||
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;
|
||||||
|
|
@ -91,9 +92,11 @@ 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 =>
|
||||||
|
|
@ -143,7 +146,13 @@ builder.Services
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
builder.Services.AddAuthorization();
|
builder.Services.AddAuthorization(options =>
|
||||||
|
{
|
||||||
|
options.AddPolicy("SystemAdmin", policy =>
|
||||||
|
{
|
||||||
|
policy.RequireRole(SystemTenantConstants.SystemRole);
|
||||||
|
});
|
||||||
|
});
|
||||||
builder.Services.AddRateLimiter(options =>
|
builder.Services.AddRateLimiter(options =>
|
||||||
{
|
{
|
||||||
options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
|
options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
|
||||||
|
|
@ -193,3 +202,7 @@ 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
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
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.TenantId;
|
var tenantId = _tenantProvider.ActorTenantId;
|
||||||
if (tenantId == null)
|
if (tenantId == null)
|
||||||
{
|
{
|
||||||
return new List<AuditLog>();
|
return new List<AuditLog>();
|
||||||
|
|
@ -88,6 +88,12 @@ 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())
|
||||||
|
|
@ -109,6 +115,9 @@ 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,
|
||||||
|
|
@ -119,6 +128,7 @@ 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
|
||||||
|
|
@ -138,6 +148,16 @@ 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
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,180 @@
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
namespace line_gestao_api.Services;
|
||||||
|
|
||||||
|
public interface ISystemAuditService
|
||||||
|
{
|
||||||
|
Task LogAsync(string action, Guid targetTenantId, object? metadata = null, CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,8 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
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";
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,115 @@
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
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,8 +13,14 @@ 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;
|
||||||
|
|
@ -27,4 +33,21 @@ 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,7 +49,10 @@ public class VigenciaNotificationBackgroundService : BackgroundService
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var tenants = await db.Tenants.AsNoTracking().ToListAsync(stoppingToken);
|
var tenants = await db.Tenants
|
||||||
|
.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,6 +83,8 @@ 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);
|
||||||
|
|
@ -213,6 +215,112 @@ 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,
|
||||||
|
|
@ -349,6 +457,38 @@ 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,
|
||||||
|
|
@ -370,6 +510,59 @@ 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=linegestao_app;Password=1231234512Ed@"
|
"Default": "Host=localhost;Port=5432;Database=linegestao;Username=postgres;Password=postgres"
|
||||||
},
|
},
|
||||||
"Jwt": {
|
"Jwt": {
|
||||||
"Key": "linegestao-local-jwt-key-2026-dev-strong-123456789",
|
"Key": "linegestao-local-jwt-key-2026-dev-strong-123456789",
|
||||||
|
|
@ -31,9 +31,8 @@
|
||||||
"Seed": {
|
"Seed": {
|
||||||
"Enabled": true,
|
"Enabled": true,
|
||||||
"ReapplyAdminCredentialsOnStartup": true,
|
"ReapplyAdminCredentialsOnStartup": true,
|
||||||
"DefaultTenantName": "Default",
|
"AdminMasterName": "Admin Master",
|
||||||
"AdminName": "Administrador",
|
"AdminMasterEmail": "admin.master@linegestao.local",
|
||||||
"AdminEmail": "admin@linegestao.local",
|
"AdminMasterPassword": "DevAdminMaster123!"
|
||||||
"AdminPassword": "DevAdmin123!"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"ConnectionStrings": {
|
"ConnectionStrings": {
|
||||||
"Default": "Host=localhost;Port=5432;Database=linegestao;Username=linegestao_app;Password=1231234512Ed@"
|
"Default": "Host=localhost;Port=5432;Database=linegestao;Username=postgres;Password=postgres"
|
||||||
},
|
},
|
||||||
"Jwt": {
|
"Jwt": {
|
||||||
"Key": "YOUR_LOCAL_STRONG_JWT_KEY_MIN_32_CHARS",
|
"Key": "YOUR_LOCAL_STRONG_JWT_KEY_MIN_32_CHARS",
|
||||||
|
|
@ -11,9 +11,8 @@
|
||||||
"Seed": {
|
"Seed": {
|
||||||
"Enabled": true,
|
"Enabled": true,
|
||||||
"ReapplyAdminCredentialsOnStartup": false,
|
"ReapplyAdminCredentialsOnStartup": false,
|
||||||
"DefaultTenantName": "Default",
|
"AdminMasterName": "Admin Master",
|
||||||
"AdminName": "Administrador",
|
"AdminMasterEmail": "admin.master@linegestao.local",
|
||||||
"AdminEmail": "admin@linegestao.local",
|
"AdminMasterPassword": "YOUR_LOCAL_ADMIN_MASTER_SEED_PASSWORD"
|
||||||
"AdminPassword": "YOUR_LOCAL_ADMIN_SEED_PASSWORD"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"ConnectionStrings": {
|
"ConnectionStrings": {
|
||||||
"Default": "Host=localhost;Port=5432;Database=linegestao;Username=linegestao_app;Password=1231234512Ed@"
|
"Default": "Host=localhost;Port=5432;Database=linegestao;Username=postgres;Password=postgres"
|
||||||
},
|
},
|
||||||
"Jwt": {
|
"Jwt": {
|
||||||
"Key": "linegestao-local-jwt-key-2026-dev-strong-123456789",
|
"Key": "linegestao-local-jwt-key-2026-dev-strong-123456789",
|
||||||
|
|
@ -31,9 +31,8 @@
|
||||||
"Seed": {
|
"Seed": {
|
||||||
"Enabled": true,
|
"Enabled": true,
|
||||||
"ReapplyAdminCredentialsOnStartup": true,
|
"ReapplyAdminCredentialsOnStartup": true,
|
||||||
"DefaultTenantName": "Default",
|
"AdminMasterName": "Admin Master",
|
||||||
"AdminName": "Administrador",
|
"AdminMasterEmail": "admin.master@linegestao.local",
|
||||||
"AdminEmail": "admin@linegestao.local",
|
"AdminMasterPassword": "DevAdminMaster123!"
|
||||||
"AdminPassword": "DevAdmin123!"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -148,8 +148,12 @@ 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;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,335 @@
|
||||||
|
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,6 +8,7 @@
|
||||||
</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" />
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,395 @@
|
||||||
|
{
|
||||||
|
"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);",
|
||||||
|
" });",
|
||||||
|
"}"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
{
|
||||||
|
"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