Feat: Aplicando Alterações/Ajustes
This commit is contained in:
parent
1f255888b0
commit
9d7306c395
|
|
@ -1,4 +1,5 @@
|
|||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Globalization;
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
using line_gestao_api.Data;
|
||||
|
|
@ -22,17 +23,20 @@ public class AuthController : ControllerBase
|
|||
private readonly AppDbContext _db;
|
||||
private readonly ITenantProvider _tenantProvider;
|
||||
private readonly IConfiguration _config;
|
||||
private readonly ILogger<AuthController> _logger;
|
||||
|
||||
public AuthController(
|
||||
UserManager<ApplicationUser> userManager,
|
||||
AppDbContext db,
|
||||
ITenantProvider tenantProvider,
|
||||
IConfiguration config)
|
||||
IConfiguration config,
|
||||
ILogger<AuthController> logger)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_db = db;
|
||||
_tenantProvider = tenantProvider;
|
||||
_config = config;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
[HttpPost("register")]
|
||||
|
|
@ -75,6 +79,8 @@ public class AuthController : ControllerBase
|
|||
if (!effectiveTenantId.HasValue)
|
||||
return Unauthorized("Tenant inválido.");
|
||||
|
||||
await EnsureClientTenantDataBoundAsync(user, effectiveTenantId.Value);
|
||||
|
||||
var token = await GenerateJwtAsync(user, effectiveTenantId.Value);
|
||||
return Ok(new AuthResponse(token));
|
||||
}
|
||||
|
|
@ -141,6 +147,8 @@ public class AuthController : ControllerBase
|
|||
if (!effectiveTenantId.HasValue)
|
||||
return Unauthorized("Tenant inválido.");
|
||||
|
||||
await EnsureClientTenantDataBoundAsync(user, effectiveTenantId.Value);
|
||||
|
||||
var token = await GenerateJwtAsync(user, effectiveTenantId.Value);
|
||||
return Ok(new AuthResponse(token));
|
||||
}
|
||||
|
|
@ -208,4 +216,200 @@ public class AuthController : ControllerBase
|
|||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -528,6 +528,7 @@ namespace line_gestao_api.Controllers
|
|||
line.Skil,
|
||||
line.Modalidade,
|
||||
line.VencConta,
|
||||
line.FranquiaLine,
|
||||
line.GestaoVozDados,
|
||||
line.Skeelo,
|
||||
line.VivoNewsPlus,
|
||||
|
|
@ -592,6 +593,7 @@ namespace line_gestao_api.Controllers
|
|||
Skil = x.Skil,
|
||||
Modalidade = x.Modalidade,
|
||||
VencConta = x.VencConta,
|
||||
FranquiaLine = x.FranquiaLine,
|
||||
GestaoVozDados = x.GestaoVozDados,
|
||||
Skeelo = x.Skeelo,
|
||||
VivoNewsPlus = x.VivoNewsPlus,
|
||||
|
|
@ -660,6 +662,7 @@ namespace line_gestao_api.Controllers
|
|||
Skil = x.Skil,
|
||||
Modalidade = x.Modalidade,
|
||||
VencConta = x.VencConta,
|
||||
FranquiaLine = x.FranquiaLine,
|
||||
GestaoVozDados = x.GestaoVozDados,
|
||||
Skeelo = x.Skeelo,
|
||||
VivoNewsPlus = x.VivoNewsPlus,
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
using System.Text.RegularExpressions;
|
||||
using System.Linq;
|
||||
using line_gestao_api.Data;
|
||||
using line_gestao_api.Dtos;
|
||||
using line_gestao_api.Services;
|
||||
|
|
@ -12,6 +14,8 @@ namespace line_gestao_api.Controllers;
|
|||
[Authorize]
|
||||
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 SpreadsheetImportAuditService _spreadsheetImportAuditService;
|
||||
|
||||
|
|
@ -23,6 +27,36 @@ public class ResumoController : ControllerBase
|
|||
|
||||
[HttpGet]
|
||||
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()
|
||||
.OrderBy(x => x.Ddd)
|
||||
|
|
@ -177,6 +211,302 @@ public class ResumoController : ControllerBase
|
|||
response.VivoLineTotals ??= new ResumoVivoLineTotalDto();
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ 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;
|
||||
|
||||
|
|
@ -85,6 +87,27 @@ public class SystemTenantUsersController : ControllerBase
|
|||
"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(
|
||||
|
|
@ -189,6 +212,12 @@ public class SystemTenantUsersController : ControllerBase
|
|||
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,
|
||||
|
|
@ -196,7 +225,8 @@ public class SystemTenantUsersController : ControllerBase
|
|||
{
|
||||
createdUserId = user.Id,
|
||||
email,
|
||||
roles = normalizedRoles
|
||||
roles = normalizedRoles,
|
||||
linesReassigned
|
||||
});
|
||||
|
||||
var response = new SystemTenantUserCreatedDto
|
||||
|
|
@ -223,4 +253,172 @@ public class SystemTenantUsersController : ControllerBase
|
|||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -133,7 +133,9 @@ public class UsersController : ControllerBase
|
|||
page = page < 1 ? 1 : page;
|
||||
pageSize = pageSize < 1 ? 20 : pageSize;
|
||||
|
||||
var usersQuery = _userManager.Users.AsNoTracking();
|
||||
var usersQuery = _userManager.Users
|
||||
.IgnoreQueryFilters()
|
||||
.AsNoTracking();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(search))
|
||||
{
|
||||
|
|
@ -195,7 +197,10 @@ public class UsersController : ControllerBase
|
|||
[Authorize(Roles = "sysadmin")]
|
||||
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)
|
||||
{
|
||||
return NotFound();
|
||||
|
|
@ -225,7 +230,9 @@ public class UsersController : ControllerBase
|
|||
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)
|
||||
{
|
||||
return NotFound();
|
||||
|
|
@ -299,11 +306,6 @@ public class UsersController : ControllerBase
|
|||
[Authorize(Roles = "sysadmin")]
|
||||
public async Task<IActionResult> Delete(Guid id)
|
||||
{
|
||||
if (_tenantProvider.TenantId == null)
|
||||
{
|
||||
return Unauthorized();
|
||||
}
|
||||
|
||||
var currentUserId = GetCurrentUserId();
|
||||
if (currentUserId.HasValue && currentUserId.Value == id)
|
||||
{
|
||||
|
|
@ -316,12 +318,14 @@ public class UsersController : ControllerBase
|
|||
});
|
||||
}
|
||||
|
||||
var tenantId = _tenantProvider.TenantId.Value;
|
||||
var user = await _userManager.Users.FirstOrDefaultAsync(u => u.Id == id && u.TenantId == tenantId);
|
||||
var user = await _userManager.Users
|
||||
.IgnoreQueryFilters()
|
||||
.FirstOrDefaultAsync(u => u.Id == id);
|
||||
if (user == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
var tenantId = user.TenantId;
|
||||
|
||||
if (user.IsActive)
|
||||
{
|
||||
|
|
@ -423,6 +427,10 @@ public class UsersController : ControllerBase
|
|||
private async Task<List<ValidationErrorDto>> ValidateUpdateAsync(Guid userId, UserUpdateRequest req)
|
||||
{
|
||||
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)
|
||||
{
|
||||
|
|
@ -434,14 +442,15 @@ public class UsersController : ControllerBase
|
|||
var email = req.Email.Trim().ToLowerInvariant();
|
||||
var normalized = _userManager.NormalizeEmail(email);
|
||||
|
||||
var tenantId = _tenantProvider.TenantId;
|
||||
if (tenantId == null)
|
||||
if (targetUser == null)
|
||||
{
|
||||
errors.Add(new ValidationErrorDto { Field = "email", Message = "Tenant inválido." });
|
||||
return errors;
|
||||
}
|
||||
else
|
||||
{
|
||||
var exists = await _userManager.Users.AnyAsync(u =>
|
||||
|
||||
var tenantId = targetUser.TenantId;
|
||||
var exists = await _userManager.Users
|
||||
.IgnoreQueryFilters()
|
||||
.AnyAsync(u =>
|
||||
u.Id != userId &&
|
||||
u.TenantId == tenantId &&
|
||||
u.NormalizedEmail == normalized);
|
||||
|
|
@ -451,7 +460,6 @@ public class UsersController : ControllerBase
|
|||
errors.Add(new ValidationErrorDto { Field = "email", Message = "E-mail já cadastrado." });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(req.Senha))
|
||||
{
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@
|
|||
public string? Skil { get; set; }
|
||||
public string? Modalidade { get; set; }
|
||||
public string? VencConta { get; set; }
|
||||
public decimal? FranquiaLine { get; set; }
|
||||
|
||||
// Campos para filtro deterministico de adicionais no frontend
|
||||
public decimal? GestaoVozDados { get; set; }
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ public class CreateSystemTenantUserRequest
|
|||
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
|
||||
|
|
|
|||
Loading…
Reference in New Issue