diff --git a/Controllers/LinesController.cs b/Controllers/LinesController.cs index ed23f17..bdefe31 100644 --- a/Controllers/LinesController.cs +++ b/Controllers/LinesController.cs @@ -719,77 +719,83 @@ namespace line_gestao_api.Controllers if (string.IsNullOrWhiteSpace(linhaLimpa)) return BadRequest(new { message = "Número de linha inválido." }); - var exists = await _db.MobileLines.AsNoTracking().AnyAsync(x => x.Linha == linhaLimpa); - if (exists) - return Conflict(new { message = $"A linha {req.Linha} já está cadastrada no sistema." }); + MobileLine? lineToPersist = null; + + if (req.ReservaLineId.HasValue && req.ReservaLineId.Value != Guid.Empty) + { + lineToPersist = await _db.MobileLines.FirstOrDefaultAsync(x => x.Id == req.ReservaLineId.Value); + if (lineToPersist == null) + return BadRequest(new { message = "A linha selecionada na Reserva não foi encontrada." }); + + if (!IsReservaLineForTransfer(lineToPersist)) + return Conflict(new { message = "A linha selecionada não está mais disponível na Reserva." }); + + if (!string.IsNullOrWhiteSpace(lineToPersist.Linha) && + !string.Equals(lineToPersist.Linha, linhaLimpa, StringComparison.Ordinal)) + { + return BadRequest(new { message = "A linha selecionada na Reserva não corresponde ao número informado." }); + } + } + + if (lineToPersist == null) + { + var existingByLinha = await _db.MobileLines.FirstOrDefaultAsync(x => x.Linha == linhaLimpa); + if (existingByLinha != null) + { + if (IsReservaLineForTransfer(existingByLinha)) + { + lineToPersist = existingByLinha; + } + else + { + return Conflict(new { message = $"A linha {req.Linha} já está cadastrada no sistema." }); + } + } + } if (!string.IsNullOrWhiteSpace(chipLimpo)) { - var chipExists = await _db.MobileLines.AsNoTracking().AnyAsync(x => x.Chip == chipLimpo); + var chipExists = await _db.MobileLines.AsNoTracking() + .AnyAsync(x => x.Chip == chipLimpo && (lineToPersist == null || x.Id != lineToPersist.Id)); if (chipExists) return Conflict(new { message = $"O Chip (ICCID) {req.Chip} já está cadastrado no sistema." }); } - var maxItem = await _db.MobileLines.MaxAsync(x => (int?)x.Item) ?? 0; - var nextItem = maxItem + 1; - var now = DateTime.UtcNow; var planSuggestion = await AutoFillRules.ResolvePlanSuggestionAsync(_db, req.PlanoContrato); var franquiaVivo = req.FranquiaVivo ?? planSuggestion?.FranquiaGb; var valorPlanoVivo = req.ValorPlanoVivo ?? planSuggestion?.ValorPlano; - var newLine = new MobileLine + var isReassignmentFromReserva = lineToPersist != null; + if (!isReassignmentFromReserva) { - Id = Guid.NewGuid(), - Item = nextItem, - Cliente = req.Cliente.Trim().ToUpper(), - Linha = linhaLimpa, - Chip = string.IsNullOrWhiteSpace(chipLimpo) ? null : chipLimpo, - Usuario = req.Usuario?.Trim(), - Status = req.Status?.Trim(), - Skil = req.Skil?.Trim(), - Modalidade = req.Modalidade?.Trim(), - PlanoContrato = req.PlanoContrato?.Trim(), - Conta = req.Conta?.Trim(), - VencConta = req.VencConta?.Trim(), + var maxItem = await _db.MobileLines.MaxAsync(x => (int?)x.Item) ?? 0; + lineToPersist = new MobileLine + { + Id = Guid.NewGuid(), + Item = maxItem + 1, + CreatedAt = now + }; + _db.MobileLines.Add(lineToPersist); + } - DataBloqueio = ToUtc(req.DataBloqueio), - DataEntregaOpera = ToUtc(req.DataEntregaOpera), - DataEntregaCliente = ToUtc(req.DataEntregaCliente), + var previousLinha = lineToPersist!.Linha; + ApplyCreateRequestToLine(lineToPersist, req, linhaLimpa, chipLimpo, franquiaVivo, valorPlanoVivo, now); + ApplyReservaRule(lineToPersist); + var ensuredTenant = await EnsureTenantForClientAsync(lineToPersist.Cliente); + if (ensuredTenant != null) + { + lineToPersist.TenantId = ensuredTenant.Id; + lineToPersist.Cliente = ensuredTenant.NomeOficial; + } - Cedente = req.Cedente?.Trim(), - Solicitante = req.Solicitante?.Trim(), - - FranquiaVivo = franquiaVivo, - ValorPlanoVivo = valorPlanoVivo, - GestaoVozDados = req.GestaoVozDados, - Skeelo = req.Skeelo, - VivoNewsPlus = req.VivoNewsPlus, - VivoTravelMundo = req.VivoTravelMundo, - VivoSync = req.VivoSync, - VivoGestaoDispositivo = req.VivoGestaoDispositivo, - ValorContratoVivo = req.ValorContratoVivo, - FranquiaLine = req.FranquiaLine, - FranquiaGestao = req.FranquiaGestao, - LocacaoAp = req.LocacaoAp, - ValorContratoLine = req.ValorContratoLine, - Desconto = req.Desconto, - Lucro = req.Lucro, - TipoDeChip = req.TipoDeChip?.Trim(), - - CreatedAt = now, - UpdatedAt = now - }; - - ApplyReservaRule(newLine); - - _db.MobileLines.Add(newLine); var vigencia = await UpsertVigenciaFromMobileLineAsync( - newLine, + lineToPersist, req.DtEfetivacaoServico, req.DtTerminoFidelizacao, - overrideDates: false); + overrideDates: false, + previousLinha: isReassignmentFromReserva ? previousLinha : null); try { @@ -801,7 +807,12 @@ namespace line_gestao_api.Controllers return StatusCode(500, new { message = "Erro ao salvar no banco de dados." }); } - return CreatedAtAction(nameof(GetById), new { id = newLine.Id }, ToDetailDto(newLine, vigencia)); + if (isReassignmentFromReserva) + { + return Ok(ToDetailDto(lineToPersist, vigencia)); + } + + return CreatedAtAction(nameof(GetById), new { id = lineToPersist.Id }, ToDetailDto(lineToPersist, vigencia)); } // ========================================================== @@ -853,6 +864,7 @@ namespace line_gestao_api.Controllers var nextItem = (await _db.MobileLines.MaxAsync(x => (int?)x.Item) ?? 0); var seenBatchLinhas = new HashSet(StringComparer.Ordinal); var seenBatchChips = new HashSet(StringComparer.Ordinal); + var tenantCache = new Dictionary(StringComparer.Ordinal); var createdLines = new List<(MobileLine line, VigenciaLine? vigencia)>(requests.Count); for (var i = 0; i < requests.Count; i++) @@ -907,7 +919,7 @@ namespace line_gestao_api.Controllers { Id = Guid.NewGuid(), Item = nextItem, - Cliente = entry.Cliente.Trim().ToUpper(), + Cliente = entry.Cliente.Trim(), Linha = linhaLimpa, Chip = string.IsNullOrWhiteSpace(chipLimpo) ? null : chipLimpo, Usuario = entry.Usuario?.Trim(), @@ -948,6 +960,19 @@ namespace line_gestao_api.Controllers ApplyReservaRule(newLine); + var tenantCacheKey = NormalizeTenantKeyValue(newLine.Cliente ?? string.Empty); + if (!tenantCache.TryGetValue(tenantCacheKey, out var ensuredTenant)) + { + ensuredTenant = await EnsureTenantForClientAsync(newLine.Cliente); + tenantCache[tenantCacheKey] = ensuredTenant; + } + + if (ensuredTenant != null) + { + newLine.TenantId = ensuredTenant.Id; + newLine.Cliente = ensuredTenant.NomeOficial; + } + _db.MobileLines.Add(newLine); var vigencia = await UpsertVigenciaFromMobileLineAsync( @@ -1041,7 +1066,8 @@ namespace line_gestao_api.Controllers var usuarioDestino = string.IsNullOrWhiteSpace(req?.UsuarioDestino) ? null : req!.UsuarioDestino!.Trim(); var skilDestinoSolicitado = string.IsNullOrWhiteSpace(req?.SkilDestino) ? null : req!.SkilDestino!.Trim(); - var clienteDestinoUpper = clienteDestino.ToUpperInvariant(); + var ensuredTenantDestino = await EnsureTenantForClientAsync(clienteDestino); + var clienteDestinoOficial = ensuredTenantDestino?.NomeOficial ?? clienteDestino; var linhas = await _db.MobileLines .Where(x => ids.Contains(x.Id)) @@ -1094,7 +1120,11 @@ namespace line_gestao_api.Controllers } var clienteAnterior = line.Cliente; - line.Cliente = clienteDestinoUpper; + line.Cliente = clienteDestinoOficial; + if (ensuredTenantDestino != null) + { + line.TenantId = ensuredTenantDestino.Id; + } if (IsReservaValue(line.Usuario)) { @@ -1334,6 +1364,12 @@ namespace line_gestao_api.Controllers } ApplyReservaRule(x); + var ensuredTenant = await EnsureTenantForClientAsync(x.Cliente); + if (ensuredTenant != null) + { + x.TenantId = ensuredTenant.Id; + x.Cliente = ensuredTenant.NomeOficial; + } await UpsertVigenciaFromMobileLineAsync( x, @@ -4049,6 +4085,167 @@ namespace line_gestao_api.Controllers || IsReservaValue(line.Cliente); } + private static void ApplyCreateRequestToLine( + MobileLine line, + CreateMobileLineDto req, + string linhaLimpa, + string? chipLimpo, + decimal? franquiaVivo, + decimal? valorPlanoVivo, + DateTime now) + { + line.Cliente = req.Cliente?.Trim(); + line.Linha = linhaLimpa; + line.Chip = string.IsNullOrWhiteSpace(chipLimpo) ? null : chipLimpo; + line.Usuario = req.Usuario?.Trim(); + line.Status = req.Status?.Trim(); + line.Skil = req.Skil?.Trim(); + line.Modalidade = req.Modalidade?.Trim(); + line.PlanoContrato = req.PlanoContrato?.Trim(); + line.Conta = req.Conta?.Trim(); + line.VencConta = req.VencConta?.Trim(); + + line.DataBloqueio = ToUtc(req.DataBloqueio); + line.DataEntregaOpera = ToUtc(req.DataEntregaOpera); + line.DataEntregaCliente = ToUtc(req.DataEntregaCliente); + + line.Cedente = req.Cedente?.Trim(); + line.Solicitante = req.Solicitante?.Trim(); + + line.FranquiaVivo = franquiaVivo; + line.ValorPlanoVivo = valorPlanoVivo; + line.GestaoVozDados = req.GestaoVozDados; + line.Skeelo = req.Skeelo; + line.VivoNewsPlus = req.VivoNewsPlus; + line.VivoTravelMundo = req.VivoTravelMundo; + line.VivoSync = req.VivoSync; + line.VivoGestaoDispositivo = req.VivoGestaoDispositivo; + line.ValorContratoVivo = req.ValorContratoVivo; + line.FranquiaLine = req.FranquiaLine; + line.FranquiaGestao = req.FranquiaGestao; + line.LocacaoAp = req.LocacaoAp; + line.ValorContratoLine = req.ValorContratoLine; + line.Desconto = req.Desconto; + line.Lucro = req.Lucro; + line.TipoDeChip = req.TipoDeChip?.Trim(); + line.UpdatedAt = now; + } + + private async Task EnsureTenantForClientAsync(string? rawClientName) + { + var clientName = (rawClientName ?? string.Empty).Trim(); + if (string.IsNullOrWhiteSpace(clientName) || IsReservaValue(clientName)) + { + return null; + } + + var normalizedClient = NormalizeTenantKeyValue(clientName); + if (string.IsNullOrWhiteSpace(normalizedClient) || + string.Equals(normalizedClient, "RESERVA", StringComparison.Ordinal)) + { + return null; + } + + var existingCandidates = await _db.Tenants + .IgnoreQueryFilters() + .Where(t => !t.IsSystem && t.SourceType == SystemTenantConstants.MobileLinesClienteSourceType) + .ToListAsync(); + + var tenant = existingCandidates.FirstOrDefault(t => + string.Equals(NormalizeTenantKeyValue(t.SourceKey ?? string.Empty), normalizedClient, StringComparison.Ordinal) || + string.Equals(NormalizeTenantKeyValue(t.NomeOficial ?? string.Empty), normalizedClient, StringComparison.Ordinal)); + + if (tenant != null) + { + var changed = false; + if (!tenant.Ativo) + { + tenant.Ativo = true; + changed = true; + } + + if (string.IsNullOrWhiteSpace(tenant.NomeOficial)) + { + tenant.NomeOficial = clientName; + changed = true; + } + + if (string.IsNullOrWhiteSpace(tenant.SourceKey)) + { + tenant.SourceKey = clientName; + changed = true; + } + + if (!string.Equals(tenant.SourceType, SystemTenantConstants.MobileLinesClienteSourceType, StringComparison.OrdinalIgnoreCase)) + { + tenant.SourceType = SystemTenantConstants.MobileLinesClienteSourceType; + changed = true; + } + + if (changed) + { + tenant.NomeOficial = tenant.NomeOficial.Trim(); + tenant.SourceKey = tenant.SourceKey?.Trim(); + } + + return tenant; + } + + var deterministicId = DeterministicGuid.FromString($"{SystemTenantConstants.MobileLinesClienteSourceType}:{normalizedClient}"); + var idAlreadyExists = await _db.Tenants.IgnoreQueryFilters().AnyAsync(t => t.Id == deterministicId); + + tenant = new Tenant + { + Id = idAlreadyExists ? Guid.NewGuid() : deterministicId, + NomeOficial = clientName, + IsSystem = false, + Ativo = true, + SourceType = SystemTenantConstants.MobileLinesClienteSourceType, + SourceKey = clientName, + CreatedAt = DateTime.UtcNow + }; + + _db.Tenants.Add(tenant); + return tenant; + } + + private static string NormalizeTenantKeyValue(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(); + } + // ========================================================== // HELPERS (SEUS) // ========================================================== diff --git a/Dtos/CreateMobileLineDto.cs b/Dtos/CreateMobileLineDto.cs index 933981f..ccc9e89 100644 --- a/Dtos/CreateMobileLineDto.cs +++ b/Dtos/CreateMobileLineDto.cs @@ -12,6 +12,7 @@ namespace line_gestao_api.Dtos public string? Chip { get; set; } // ICCID public string? Cliente { get; set; } // Obrigatório na validação do Controller public string? Usuario { get; set; } + public Guid? ReservaLineId { get; set; } // Reaproveita linha já existente na Reserva // ========================== // Classificação e Status