Compare commits
11 Commits
355267b740
...
ae378ba9cc
| Author | SHA1 | Date |
|---|---|---|
|
|
ae378ba9cc | |
|
|
6af6674468 | |
|
|
d37ea0c377 | |
|
|
d4ec1f72a6 | |
|
|
78f403105c | |
|
|
22ab6997d3 | |
|
|
64ffb9f2e5 | |
|
|
82dd0bf2d0 | |
|
|
8a562777df | |
|
|
208c201156 | |
|
|
3e6319566b |
|
|
@ -10,7 +10,7 @@ namespace line_gestao_api.Controllers
|
||||||
{
|
{
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("api/[controller]")]
|
[Route("api/[controller]")]
|
||||||
[Authorize(Roles = "sysadmin,gestor")]
|
[Authorize(Roles = "sysadmin,financeiro")]
|
||||||
public class BillingController : ControllerBase
|
public class BillingController : ControllerBase
|
||||||
{
|
{
|
||||||
private readonly AppDbContext _db;
|
private readonly AppDbContext _db;
|
||||||
|
|
@ -197,7 +197,7 @@ namespace line_gestao_api.Controllers
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPut("{id:guid}")]
|
[HttpPut("{id:guid}")]
|
||||||
[Authorize(Roles = "sysadmin,gestor")]
|
[Authorize(Roles = "sysadmin")]
|
||||||
public async Task<IActionResult> Update(Guid id, [FromBody] UpdateBillingClientRequest req)
|
public async Task<IActionResult> Update(Guid id, [FromBody] UpdateBillingClientRequest req)
|
||||||
{
|
{
|
||||||
var x = await _db.BillingClients.FirstOrDefaultAsync(a => a.Id == id);
|
var x = await _db.BillingClients.FirstOrDefaultAsync(a => a.Id == id);
|
||||||
|
|
@ -230,7 +230,7 @@ namespace line_gestao_api.Controllers
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpDelete("{id:guid}")]
|
[HttpDelete("{id:guid}")]
|
||||||
[Authorize(Roles = "sysadmin,gestor")]
|
[Authorize(Roles = "sysadmin")]
|
||||||
public async Task<IActionResult> Delete(Guid id)
|
public async Task<IActionResult> Delete(Guid id)
|
||||||
{
|
{
|
||||||
var x = await _db.BillingClients.FirstOrDefaultAsync(a => a.Id == id);
|
var x = await _db.BillingClients.FirstOrDefaultAsync(a => a.Id == id);
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ namespace line_gestao_api.Controllers
|
||||||
{
|
{
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("api/chips-virgens")]
|
[Route("api/chips-virgens")]
|
||||||
[Authorize(Roles = "sysadmin,gestor")]
|
[Authorize(Roles = "sysadmin,gestor,financeiro")]
|
||||||
public class ChipsVirgensController : ControllerBase
|
public class ChipsVirgensController : ControllerBase
|
||||||
{
|
{
|
||||||
private readonly AppDbContext _db;
|
private readonly AppDbContext _db;
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ namespace line_gestao_api.Controllers
|
||||||
{
|
{
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("api/controle-recebidos")]
|
[Route("api/controle-recebidos")]
|
||||||
[Authorize(Roles = "sysadmin,gestor")]
|
[Authorize(Roles = "sysadmin,gestor,financeiro")]
|
||||||
public class ControleRecebidosController : ControllerBase
|
public class ControleRecebidosController : ControllerBase
|
||||||
{
|
{
|
||||||
private readonly AppDbContext _db;
|
private readonly AppDbContext _db;
|
||||||
|
|
|
||||||
|
|
@ -18,9 +18,9 @@ namespace line_gestao_api.Controllers
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("insights")]
|
[HttpGet("insights")]
|
||||||
public async Task<ActionResult<GeralDashboardInsightsDto>> GetInsights()
|
public async Task<ActionResult<GeralDashboardInsightsDto>> GetInsights([FromQuery] string? operadora = null)
|
||||||
{
|
{
|
||||||
var dto = await _service.GetInsightsAsync();
|
var dto = await _service.GetInsightsAsync(operadora);
|
||||||
return Ok(dto);
|
return Ok(dto);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,38 @@
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using line_gestao_api.Data;
|
using line_gestao_api.Data;
|
||||||
using line_gestao_api.Dtos;
|
using line_gestao_api.Dtos;
|
||||||
|
using line_gestao_api.Models;
|
||||||
using line_gestao_api.Services;
|
using line_gestao_api.Services;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
namespace line_gestao_api.Controllers;
|
namespace line_gestao_api.Controllers;
|
||||||
|
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("api/historico")]
|
[Route("api/historico")]
|
||||||
[Authorize(Roles = "sysadmin,gestor")]
|
[Authorize(Roles = "sysadmin,gestor,financeiro")]
|
||||||
public class HistoricoController : ControllerBase
|
public class HistoricoController : ControllerBase
|
||||||
{
|
{
|
||||||
|
private static readonly HashSet<string> LineRelatedEntities = new(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
nameof(MobileLine),
|
||||||
|
nameof(MuregLine),
|
||||||
|
nameof(TrocaNumeroLine),
|
||||||
|
nameof(VigenciaLine),
|
||||||
|
nameof(ParcelamentoLine)
|
||||||
|
};
|
||||||
|
|
||||||
|
private static readonly HashSet<string> ChipRelatedEntities = new(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
nameof(MobileLine),
|
||||||
|
nameof(TrocaNumeroLine),
|
||||||
|
nameof(ChipVirgemLine),
|
||||||
|
nameof(ControleRecebidoLine)
|
||||||
|
};
|
||||||
|
|
||||||
private readonly AppDbContext _db;
|
private readonly AppDbContext _db;
|
||||||
|
|
||||||
public HistoricoController(AppDbContext db)
|
public HistoricoController(AppDbContext db)
|
||||||
|
|
@ -121,11 +140,224 @@ public class HistoricoController : ControllerBase
|
||||||
Page = page,
|
Page = page,
|
||||||
PageSize = pageSize,
|
PageSize = pageSize,
|
||||||
Total = total,
|
Total = total,
|
||||||
Items = items.Select(ToDto).ToList()
|
Items = items.Select(log => ToDto(log)).ToList()
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private static AuditLogDto ToDto(Models.AuditLog log)
|
[HttpGet("linhas")]
|
||||||
|
public async Task<ActionResult<PagedResult<AuditLogDto>>> GetLineHistory(
|
||||||
|
[FromQuery] string? line,
|
||||||
|
[FromQuery] string? pageName,
|
||||||
|
[FromQuery] string? action,
|
||||||
|
[FromQuery] string? user,
|
||||||
|
[FromQuery] string? search,
|
||||||
|
[FromQuery] DateTime? dateFrom,
|
||||||
|
[FromQuery] DateTime? dateTo,
|
||||||
|
[FromQuery] int page = 1,
|
||||||
|
[FromQuery] int pageSize = 20)
|
||||||
|
{
|
||||||
|
page = page < 1 ? 1 : page;
|
||||||
|
pageSize = pageSize < 1 ? 20 : pageSize;
|
||||||
|
|
||||||
|
var lineTerm = (line ?? string.Empty).Trim();
|
||||||
|
var normalizedLineDigits = DigitsOnly(lineTerm);
|
||||||
|
var hasLineFilter = !string.IsNullOrWhiteSpace(lineTerm) || !string.IsNullOrWhiteSpace(normalizedLineDigits);
|
||||||
|
|
||||||
|
var q = _db.AuditLogs
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(x => LineRelatedEntities.Contains(x.EntityName))
|
||||||
|
.Where(x =>
|
||||||
|
!EF.Functions.ILike(x.RequestPath ?? "", "%import-excel%") ||
|
||||||
|
x.Page == AuditLogBuilder.SpreadsheetImportPageName);
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(pageName))
|
||||||
|
{
|
||||||
|
var p = pageName.Trim();
|
||||||
|
q = q.Where(x => EF.Functions.ILike(x.Page, $"%{p}%"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(action))
|
||||||
|
{
|
||||||
|
var a = action.Trim().ToUpperInvariant();
|
||||||
|
q = q.Where(x => x.Action == a);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(user))
|
||||||
|
{
|
||||||
|
var u = user.Trim();
|
||||||
|
q = q.Where(x =>
|
||||||
|
EF.Functions.ILike(x.UserName ?? "", $"%{u}%") ||
|
||||||
|
EF.Functions.ILike(x.UserEmail ?? "", $"%{u}%"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dateFrom.HasValue)
|
||||||
|
{
|
||||||
|
var fromUtc = ToUtc(dateFrom.Value);
|
||||||
|
q = q.Where(x => x.OccurredAtUtc >= fromUtc);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dateTo.HasValue)
|
||||||
|
{
|
||||||
|
var toUtc = ToUtc(dateTo.Value);
|
||||||
|
if (dateTo.Value.TimeOfDay == TimeSpan.Zero)
|
||||||
|
{
|
||||||
|
toUtc = toUtc.Date.AddDays(1).AddTicks(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
q = q.Where(x => x.OccurredAtUtc <= toUtc);
|
||||||
|
}
|
||||||
|
|
||||||
|
var candidateLogs = await q
|
||||||
|
.OrderByDescending(x => x.OccurredAtUtc)
|
||||||
|
.ThenByDescending(x => x.Id)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
var searchTerm = (search ?? string.Empty).Trim();
|
||||||
|
var searchDigits = DigitsOnly(searchTerm);
|
||||||
|
var matchedLogs = new List<(AuditLog Log, List<AuditFieldChangeDto> Changes)>();
|
||||||
|
|
||||||
|
foreach (var log in candidateLogs)
|
||||||
|
{
|
||||||
|
var changes = ParseChanges(log.ChangesJson);
|
||||||
|
if (hasLineFilter && !MatchesLine(log, changes, lineTerm, normalizedLineDigits))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(searchTerm) && !MatchesSearch(log, changes, searchTerm, searchDigits))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
matchedLogs.Add((log, changes));
|
||||||
|
}
|
||||||
|
|
||||||
|
var total = matchedLogs.Count;
|
||||||
|
var items = matchedLogs
|
||||||
|
.Skip((page - 1) * pageSize)
|
||||||
|
.Take(pageSize)
|
||||||
|
.Select(x => ToDto(x.Log, x.Changes))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
return Ok(new PagedResult<AuditLogDto>
|
||||||
|
{
|
||||||
|
Page = page,
|
||||||
|
PageSize = pageSize,
|
||||||
|
Total = total,
|
||||||
|
Items = items
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("chips")]
|
||||||
|
public async Task<ActionResult<PagedResult<AuditLogDto>>> GetChipHistory(
|
||||||
|
[FromQuery] string? chip,
|
||||||
|
[FromQuery] string? pageName,
|
||||||
|
[FromQuery] string? action,
|
||||||
|
[FromQuery] string? user,
|
||||||
|
[FromQuery] string? search,
|
||||||
|
[FromQuery] DateTime? dateFrom,
|
||||||
|
[FromQuery] DateTime? dateTo,
|
||||||
|
[FromQuery] int page = 1,
|
||||||
|
[FromQuery] int pageSize = 20)
|
||||||
|
{
|
||||||
|
page = page < 1 ? 1 : page;
|
||||||
|
pageSize = pageSize < 1 ? 20 : pageSize;
|
||||||
|
|
||||||
|
var chipTerm = (chip ?? string.Empty).Trim();
|
||||||
|
var normalizedChipDigits = DigitsOnly(chipTerm);
|
||||||
|
var hasChipFilter = !string.IsNullOrWhiteSpace(chipTerm) || !string.IsNullOrWhiteSpace(normalizedChipDigits);
|
||||||
|
|
||||||
|
var q = _db.AuditLogs
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(x => ChipRelatedEntities.Contains(x.EntityName))
|
||||||
|
.Where(x =>
|
||||||
|
!EF.Functions.ILike(x.RequestPath ?? "", "%import-excel%") ||
|
||||||
|
x.Page == AuditLogBuilder.SpreadsheetImportPageName);
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(pageName))
|
||||||
|
{
|
||||||
|
var p = pageName.Trim();
|
||||||
|
q = q.Where(x => EF.Functions.ILike(x.Page, $"%{p}%"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(action))
|
||||||
|
{
|
||||||
|
var a = action.Trim().ToUpperInvariant();
|
||||||
|
q = q.Where(x => x.Action == a);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(user))
|
||||||
|
{
|
||||||
|
var u = user.Trim();
|
||||||
|
q = q.Where(x =>
|
||||||
|
EF.Functions.ILike(x.UserName ?? "", $"%{u}%") ||
|
||||||
|
EF.Functions.ILike(x.UserEmail ?? "", $"%{u}%"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dateFrom.HasValue)
|
||||||
|
{
|
||||||
|
var fromUtc = ToUtc(dateFrom.Value);
|
||||||
|
q = q.Where(x => x.OccurredAtUtc >= fromUtc);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dateTo.HasValue)
|
||||||
|
{
|
||||||
|
var toUtc = ToUtc(dateTo.Value);
|
||||||
|
if (dateTo.Value.TimeOfDay == TimeSpan.Zero)
|
||||||
|
{
|
||||||
|
toUtc = toUtc.Date.AddDays(1).AddTicks(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
q = q.Where(x => x.OccurredAtUtc <= toUtc);
|
||||||
|
}
|
||||||
|
|
||||||
|
var candidateLogs = await q
|
||||||
|
.OrderByDescending(x => x.OccurredAtUtc)
|
||||||
|
.ThenByDescending(x => x.Id)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
var searchTerm = (search ?? string.Empty).Trim();
|
||||||
|
var searchDigits = DigitsOnly(searchTerm);
|
||||||
|
var matchedLogs = new List<(AuditLog Log, List<AuditFieldChangeDto> Changes)>();
|
||||||
|
|
||||||
|
foreach (var log in candidateLogs)
|
||||||
|
{
|
||||||
|
var changes = ParseChanges(log.ChangesJson);
|
||||||
|
if (!HasChipContext(log, changes))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasChipFilter && !MatchesChip(log, changes, chipTerm, normalizedChipDigits))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(searchTerm) && !MatchesSearch(log, changes, searchTerm, searchDigits))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
matchedLogs.Add((log, changes));
|
||||||
|
}
|
||||||
|
|
||||||
|
var total = matchedLogs.Count;
|
||||||
|
var items = matchedLogs
|
||||||
|
.Skip((page - 1) * pageSize)
|
||||||
|
.Take(pageSize)
|
||||||
|
.Select(x => ToDto(x.Log, x.Changes))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
return Ok(new PagedResult<AuditLogDto>
|
||||||
|
{
|
||||||
|
Page = page,
|
||||||
|
PageSize = pageSize,
|
||||||
|
Total = total,
|
||||||
|
Items = items
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static AuditLogDto ToDto(AuditLog log, List<AuditFieldChangeDto>? parsedChanges = null)
|
||||||
{
|
{
|
||||||
return new AuditLogDto
|
return new AuditLogDto
|
||||||
{
|
{
|
||||||
|
|
@ -142,7 +374,7 @@ public class HistoricoController : ControllerBase
|
||||||
RequestPath = log.RequestPath,
|
RequestPath = log.RequestPath,
|
||||||
RequestMethod = log.RequestMethod,
|
RequestMethod = log.RequestMethod,
|
||||||
IpAddress = log.IpAddress,
|
IpAddress = log.IpAddress,
|
||||||
Changes = ParseChanges(log.ChangesJson)
|
Changes = parsedChanges ?? ParseChanges(log.ChangesJson)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -173,6 +405,162 @@ public class HistoricoController : ControllerBase
|
||||||
return DateTime.SpecifyKind(value, DateTimeKind.Utc);
|
return DateTime.SpecifyKind(value, DateTimeKind.Utc);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool MatchesLine(
|
||||||
|
AuditLog log,
|
||||||
|
List<AuditFieldChangeDto> changes,
|
||||||
|
string lineTerm,
|
||||||
|
string normalizedLineDigits)
|
||||||
|
{
|
||||||
|
if (MatchesTerm(log.EntityLabel, lineTerm, normalizedLineDigits) ||
|
||||||
|
MatchesTerm(log.EntityId, lineTerm, normalizedLineDigits))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var change in changes)
|
||||||
|
{
|
||||||
|
if (MatchesTerm(change.Field, lineTerm, normalizedLineDigits) ||
|
||||||
|
MatchesTerm(change.OldValue, lineTerm, normalizedLineDigits) ||
|
||||||
|
MatchesTerm(change.NewValue, lineTerm, normalizedLineDigits))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool MatchesSearch(
|
||||||
|
AuditLog log,
|
||||||
|
List<AuditFieldChangeDto> changes,
|
||||||
|
string searchTerm,
|
||||||
|
string searchDigits)
|
||||||
|
{
|
||||||
|
if (MatchesTerm(log.UserName, searchTerm, searchDigits) ||
|
||||||
|
MatchesTerm(log.UserEmail, searchTerm, searchDigits) ||
|
||||||
|
MatchesTerm(log.Action, searchTerm, searchDigits) ||
|
||||||
|
MatchesTerm(log.Page, searchTerm, searchDigits) ||
|
||||||
|
MatchesTerm(log.EntityName, searchTerm, searchDigits) ||
|
||||||
|
MatchesTerm(log.EntityId, searchTerm, searchDigits) ||
|
||||||
|
MatchesTerm(log.EntityLabel, searchTerm, searchDigits) ||
|
||||||
|
MatchesTerm(log.RequestMethod, searchTerm, searchDigits) ||
|
||||||
|
MatchesTerm(log.RequestPath, searchTerm, searchDigits) ||
|
||||||
|
MatchesTerm(log.IpAddress, searchTerm, searchDigits))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var change in changes)
|
||||||
|
{
|
||||||
|
if (MatchesTerm(change.Field, searchTerm, searchDigits) ||
|
||||||
|
MatchesTerm(change.OldValue, searchTerm, searchDigits) ||
|
||||||
|
MatchesTerm(change.NewValue, searchTerm, searchDigits))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool MatchesChip(
|
||||||
|
AuditLog log,
|
||||||
|
List<AuditFieldChangeDto> changes,
|
||||||
|
string chipTerm,
|
||||||
|
string normalizedChipDigits)
|
||||||
|
{
|
||||||
|
if (MatchesTerm(log.EntityLabel, chipTerm, normalizedChipDigits) ||
|
||||||
|
MatchesTerm(log.EntityId, chipTerm, normalizedChipDigits))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var change in changes)
|
||||||
|
{
|
||||||
|
if (MatchesTerm(change.Field, chipTerm, normalizedChipDigits) ||
|
||||||
|
MatchesTerm(change.OldValue, chipTerm, normalizedChipDigits) ||
|
||||||
|
MatchesTerm(change.NewValue, chipTerm, normalizedChipDigits))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool HasChipContext(AuditLog log, List<AuditFieldChangeDto> changes)
|
||||||
|
{
|
||||||
|
if ((log.EntityName ?? string.Empty).Equals(nameof(ChipVirgemLine), StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
(log.Page ?? string.Empty).Contains("chip", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var change in changes)
|
||||||
|
{
|
||||||
|
var normalizedField = NormalizeField(change.Field);
|
||||||
|
if (normalizedField is "chip" or "iccid" or "numerodochip")
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool MatchesTerm(string? source, string term, string digitsTerm)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(source))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(term) &&
|
||||||
|
source.Contains(term, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(digitsTerm))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var sourceDigits = DigitsOnly(source);
|
||||||
|
if (string.IsNullOrWhiteSpace(sourceDigits))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return sourceDigits.Contains(digitsTerm, StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string NormalizeField(string? value)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new string(value
|
||||||
|
.Normalize(NormalizationForm.FormD)
|
||||||
|
.Where(ch => CharUnicodeInfo.GetUnicodeCategory(ch) != UnicodeCategory.NonSpacingMark)
|
||||||
|
.Where(char.IsLetterOrDigit)
|
||||||
|
.Select(char.ToLowerInvariant)
|
||||||
|
.ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string DigitsOnly(string? value)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
var chars = value.Where(char.IsDigit).ToArray();
|
||||||
|
return chars.Length == 0 ? string.Empty : new string(chars);
|
||||||
|
}
|
||||||
|
|
||||||
private static bool TryParseSearchDateUtcStart(string value, out DateTime utcStart)
|
private static bool TryParseSearchDateUtcStart(string value, out DateTime utcStart)
|
||||||
{
|
{
|
||||||
utcStart = default;
|
utcStart = default;
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -183,7 +183,7 @@ namespace line_gestao_api.Controllers
|
||||||
public string? LinhaAntiga { get; set; } // opcional (snapshot)
|
public string? LinhaAntiga { get; set; } // opcional (snapshot)
|
||||||
public string? LinhaNova { get; set; } // opcional
|
public string? LinhaNova { get; set; } // opcional
|
||||||
public string? ICCID { get; set; } // opcional
|
public string? ICCID { get; set; } // opcional
|
||||||
public DateTime? DataDaMureg { get; set; } // opcional
|
public DateTime? DataDaMureg { get; set; } // ignorado no create (sistema define automaticamente)
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
|
|
@ -234,7 +234,8 @@ namespace line_gestao_api.Controllers
|
||||||
LinhaAntiga = linhaAntigaSnapshot,
|
LinhaAntiga = linhaAntigaSnapshot,
|
||||||
LinhaNova = linhaNova,
|
LinhaNova = linhaNova,
|
||||||
ICCID = iccid,
|
ICCID = iccid,
|
||||||
DataDaMureg = ToUtc(req.DataDaMureg),
|
// Data automática no momento da criação da Mureg
|
||||||
|
DataDaMureg = now,
|
||||||
CreatedAt = now,
|
CreatedAt = now,
|
||||||
UpdatedAt = now
|
UpdatedAt = now
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,74 @@
|
||||||
|
using line_gestao_api.Dtos;
|
||||||
|
using line_gestao_api.Services;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace line_gestao_api.Controllers;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/mve-audit")]
|
||||||
|
[Authorize(Roles = "sysadmin,gestor")]
|
||||||
|
public class MveAuditController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly MveAuditService _mveAuditService;
|
||||||
|
|
||||||
|
public MveAuditController(MveAuditService mveAuditService)
|
||||||
|
{
|
||||||
|
_mveAuditService = mveAuditService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class MveAuditUploadForm
|
||||||
|
{
|
||||||
|
public IFormFile File { get; set; } = default!;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("preview")]
|
||||||
|
[Consumes("multipart/form-data")]
|
||||||
|
[RequestSizeLimit(20_000_000)]
|
||||||
|
public async Task<ActionResult<MveAuditRunDto>> Preview(
|
||||||
|
[FromForm] MveAuditUploadForm form,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = await _mveAuditService.CreateRunAsync(form.File, cancellationToken);
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
catch (InvalidOperationException ex)
|
||||||
|
{
|
||||||
|
return BadRequest(new { message = ex.Message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("{id:guid}")]
|
||||||
|
public async Task<ActionResult<MveAuditRunDto>> GetById(Guid id, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var result = await _mveAuditService.GetByIdAsync(id, cancellationToken);
|
||||||
|
return result == null ? NotFound() : Ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("latest")]
|
||||||
|
public async Task<ActionResult<MveAuditRunDto>> GetLatest(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var result = await _mveAuditService.GetLatestAsync(cancellationToken);
|
||||||
|
return result == null ? NotFound() : Ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("{id:guid}/apply")]
|
||||||
|
public async Task<ActionResult<ApplyMveAuditResultDto>> Apply(
|
||||||
|
Guid id,
|
||||||
|
[FromBody] ApplyMveAuditRequestDto request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = await _mveAuditService.ApplyAsync(id, request?.IssueIds, cancellationToken);
|
||||||
|
return result == null ? NotFound() : Ok(result);
|
||||||
|
}
|
||||||
|
catch (InvalidOperationException ex)
|
||||||
|
{
|
||||||
|
return BadRequest(new { message = ex.Message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -9,7 +9,7 @@ namespace line_gestao_api.Controllers;
|
||||||
|
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("api/parcelamentos")]
|
[Route("api/parcelamentos")]
|
||||||
[Authorize(Roles = "sysadmin,gestor")]
|
[Authorize(Roles = "sysadmin,financeiro")]
|
||||||
public class ParcelamentosController : ControllerBase
|
public class ParcelamentosController : ControllerBase
|
||||||
{
|
{
|
||||||
private readonly AppDbContext _db;
|
private readonly AppDbContext _db;
|
||||||
|
|
@ -165,7 +165,7 @@ public class ParcelamentosController : ControllerBase
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
[Authorize(Roles = "sysadmin,gestor")]
|
[Authorize(Roles = "sysadmin")]
|
||||||
public async Task<ActionResult<ParcelamentoDetailDto>> Create([FromBody] ParcelamentoUpsertDto req)
|
public async Task<ActionResult<ParcelamentoDetailDto>> Create([FromBody] ParcelamentoUpsertDto req)
|
||||||
{
|
{
|
||||||
var now = DateTime.UtcNow;
|
var now = DateTime.UtcNow;
|
||||||
|
|
@ -202,7 +202,7 @@ public class ParcelamentosController : ControllerBase
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPut("{id:guid}")]
|
[HttpPut("{id:guid}")]
|
||||||
[Authorize(Roles = "sysadmin,gestor")]
|
[Authorize(Roles = "sysadmin")]
|
||||||
public async Task<IActionResult> Update(Guid id, [FromBody] ParcelamentoUpsertDto req)
|
public async Task<IActionResult> Update(Guid id, [FromBody] ParcelamentoUpsertDto req)
|
||||||
{
|
{
|
||||||
var entity = await _db.ParcelamentoLines
|
var entity = await _db.ParcelamentoLines
|
||||||
|
|
@ -239,7 +239,7 @@ public class ParcelamentosController : ControllerBase
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpDelete("{id:guid}")]
|
[HttpDelete("{id:guid}")]
|
||||||
[Authorize(Roles = "sysadmin,gestor")]
|
[Authorize(Roles = "sysadmin")]
|
||||||
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);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
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 Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
@ -12,14 +13,18 @@ namespace line_gestao_api.Controllers
|
||||||
public class RelatoriosController : ControllerBase
|
public class RelatoriosController : ControllerBase
|
||||||
{
|
{
|
||||||
private readonly AppDbContext _db;
|
private readonly AppDbContext _db;
|
||||||
|
private readonly DashboardKpiSnapshotService _dashboardKpiSnapshotService;
|
||||||
|
|
||||||
public RelatoriosController(AppDbContext db)
|
public RelatoriosController(
|
||||||
|
AppDbContext db,
|
||||||
|
DashboardKpiSnapshotService dashboardKpiSnapshotService)
|
||||||
{
|
{
|
||||||
_db = db;
|
_db = db;
|
||||||
|
_dashboardKpiSnapshotService = dashboardKpiSnapshotService;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("dashboard")]
|
[HttpGet("dashboard")]
|
||||||
public async Task<ActionResult<RelatoriosDashboardDto>> GetDashboard()
|
public async Task<ActionResult<RelatoriosDashboardDto>> GetDashboard([FromQuery] string? operadora = null)
|
||||||
{
|
{
|
||||||
var todayUtcStart = DateTime.SpecifyKind(DateTime.UtcNow.Date, DateTimeKind.Utc);
|
var todayUtcStart = DateTime.SpecifyKind(DateTime.UtcNow.Date, DateTimeKind.Utc);
|
||||||
var tomorrowUtcStart = todayUtcStart.AddDays(1);
|
var tomorrowUtcStart = todayUtcStart.AddDays(1);
|
||||||
|
|
@ -31,32 +36,40 @@ namespace line_gestao_api.Controllers
|
||||||
// =========================
|
// =========================
|
||||||
// GERAL (MobileLines)
|
// GERAL (MobileLines)
|
||||||
// =========================
|
// =========================
|
||||||
var qLines = _db.MobileLines.AsNoTracking();
|
var qLines = OperadoraContaResolver.ApplyOperadoraFilter(_db.MobileLines.AsNoTracking(), operadora);
|
||||||
var qLinesWithClient = qLines.Where(x => x.Cliente != null && x.Cliente != "");
|
var qReserva = qLines.Where(x =>
|
||||||
|
EF.Functions.ILike((x.Usuario ?? "").Trim(), "RESERVA") ||
|
||||||
|
EF.Functions.ILike((x.Skil ?? "").Trim(), "RESERVA") ||
|
||||||
|
EF.Functions.ILike((x.Cliente ?? "").Trim(), "RESERVA"));
|
||||||
|
var qOperacionais = qLines.Where(x =>
|
||||||
|
!EF.Functions.ILike((x.Usuario ?? "").Trim(), "RESERVA") &&
|
||||||
|
!EF.Functions.ILike((x.Skil ?? "").Trim(), "RESERVA") &&
|
||||||
|
!EF.Functions.ILike((x.Cliente ?? "").Trim(), "RESERVA"));
|
||||||
|
var qOperacionaisWithClient = qOperacionais.Where(x => x.Cliente != null && x.Cliente != "");
|
||||||
|
|
||||||
var totalLinhas = await qLines.CountAsync();
|
var totalLinhas = await qLines.CountAsync();
|
||||||
|
|
||||||
var clientesUnicos = await qLines
|
var clientesUnicos = await qOperacionais
|
||||||
.Where(x => x.Cliente != null && x.Cliente != "")
|
.Where(x => x.Cliente != null && x.Cliente != "")
|
||||||
.Select(x => x.Cliente!)
|
.Select(x => x.Cliente!)
|
||||||
.Distinct()
|
.Distinct()
|
||||||
.CountAsync();
|
.CountAsync();
|
||||||
|
|
||||||
var ativos = await qLines.CountAsync(x =>
|
var ativos = await qOperacionais.CountAsync(x =>
|
||||||
EF.Functions.ILike((x.Status ?? "").Trim(), "%ativo%"));
|
EF.Functions.ILike((x.Status ?? "").Trim(), "%ativo%"));
|
||||||
|
|
||||||
var bloqueadosPerdaRoubo = await qLinesWithClient.CountAsync(x =>
|
var bloqueadosPerdaRoubo = await qOperacionaisWithClient.CountAsync(x =>
|
||||||
EF.Functions.ILike((x.Status ?? "").Trim(), "%perda%") ||
|
EF.Functions.ILike((x.Status ?? "").Trim(), "%perda%") ||
|
||||||
EF.Functions.ILike((x.Status ?? "").Trim(), "%roubo%"));
|
EF.Functions.ILike((x.Status ?? "").Trim(), "%roubo%"));
|
||||||
|
|
||||||
var bloqueados120Dias = await qLinesWithClient.CountAsync(x =>
|
var bloqueados120Dias = await qOperacionaisWithClient.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(), "%perda%") ||
|
||||||
EF.Functions.ILike((x.Status ?? "").Trim(), "%roubo%")));
|
EF.Functions.ILike((x.Status ?? "").Trim(), "%roubo%")));
|
||||||
|
|
||||||
var bloqueadosOutros = await qLinesWithClient.CountAsync(x =>
|
var bloqueadosOutros = await qOperacionaisWithClient.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%"))
|
||||||
|
|
@ -64,16 +77,13 @@ namespace line_gestao_api.Controllers
|
||||||
|
|
||||||
// Regra do KPI "Bloqueadas" alinhada ao critério da página Geral:
|
// Regra do KPI "Bloqueadas" alinhada ao critério da página Geral:
|
||||||
// status contendo "bloque", "perda" ou "roubo".
|
// status contendo "bloque", "perda" ou "roubo".
|
||||||
var bloqueados = await qLinesWithClient.CountAsync(x =>
|
var bloqueados = await qOperacionaisWithClient.CountAsync(x =>
|
||||||
EF.Functions.ILike((x.Status ?? "").Trim(), "%bloque%") ||
|
EF.Functions.ILike((x.Status ?? "").Trim(), "%bloque%") ||
|
||||||
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%"));
|
||||||
|
|
||||||
// Regra alinhada ao filtro "Reservas" da página Geral.
|
// Regra alinhada ao filtro "Reservas" da página Geral.
|
||||||
var reservas = await qLines.CountAsync(x =>
|
var reservas = await qReserva.CountAsync();
|
||||||
EF.Functions.ILike((x.Usuario ?? "").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 != "")
|
||||||
|
|
@ -173,7 +183,16 @@ namespace line_gestao_api.Controllers
|
||||||
// =========================
|
// =========================
|
||||||
// USER DATA
|
// USER DATA
|
||||||
// =========================
|
// =========================
|
||||||
var qUserData = _db.UserDatas.AsNoTracking();
|
var qLineItems = qLines
|
||||||
|
.Where(x => x.Item > 0)
|
||||||
|
.Select(x => x.Item);
|
||||||
|
var qLineNumbers = qLines
|
||||||
|
.Where(x => x.Linha != null && x.Linha != "")
|
||||||
|
.Select(x => x.Linha!);
|
||||||
|
var qUserData = _db.UserDatas.AsNoTracking()
|
||||||
|
.Where(x =>
|
||||||
|
(x.Item > 0 && qLineItems.Contains(x.Item)) ||
|
||||||
|
(x.Linha != null && x.Linha != "" && qLineNumbers.Contains(x.Linha)));
|
||||||
|
|
||||||
var userDataRegistros = await qUserData.CountAsync();
|
var userDataRegistros = await qUserData.CountAsync();
|
||||||
var userDataComCpf = await qUserData.CountAsync(x => x.Cpf != null && x.Cpf != "");
|
var userDataComCpf = await qUserData.CountAsync(x => x.Cpf != null && x.Cpf != "");
|
||||||
|
|
@ -211,6 +230,7 @@ namespace line_gestao_api.Controllers
|
||||||
UserDataComCpf = userDataComCpf,
|
UserDataComCpf = userDataComCpf,
|
||||||
UserDataComEmail = userDataComEmail
|
UserDataComEmail = userDataComEmail
|
||||||
},
|
},
|
||||||
|
KpiTrends = await _dashboardKpiSnapshotService.GetTrendMapAsync(operadora),
|
||||||
TopClientes = topClientes,
|
TopClientes = topClientes,
|
||||||
SerieMuregUltimos12Meses = serieMureg12,
|
SerieMuregUltimos12Meses = serieMureg12,
|
||||||
SerieTrocaUltimos12Meses = serieTroca12,
|
SerieTrocaUltimos12Meses = serieTroca12,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,240 @@
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Security.Claims;
|
||||||
|
using line_gestao_api.Data;
|
||||||
|
using line_gestao_api.Dtos;
|
||||||
|
using line_gestao_api.Models;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace line_gestao_api.Controllers;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/solicitacoes-linhas")]
|
||||||
|
[Authorize]
|
||||||
|
public class SolicitacoesLinhasController : ControllerBase
|
||||||
|
{
|
||||||
|
private const string TipoAlteracaoFranquia = "ALTERACAO_FRANQUIA";
|
||||||
|
private const string TipoBloqueio = "BLOQUEIO";
|
||||||
|
private readonly AppDbContext _db;
|
||||||
|
|
||||||
|
public SolicitacoesLinhasController(AppDbContext db)
|
||||||
|
{
|
||||||
|
_db = db;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
[Authorize(Roles = "sysadmin,gestor,cliente")]
|
||||||
|
public async Task<ActionResult<SolicitacaoLinhaListDto>> Create([FromBody] CreateSolicitacaoLinhaRequestDto req)
|
||||||
|
{
|
||||||
|
if (req.LineId == Guid.Empty)
|
||||||
|
{
|
||||||
|
return BadRequest(new { message = "Linha inválida para solicitação." });
|
||||||
|
}
|
||||||
|
|
||||||
|
var line = await _db.MobileLines
|
||||||
|
.AsNoTracking()
|
||||||
|
.FirstOrDefaultAsync(x => x.Id == req.LineId);
|
||||||
|
if (line == null)
|
||||||
|
{
|
||||||
|
return NotFound(new { message = "Linha não encontrada." });
|
||||||
|
}
|
||||||
|
|
||||||
|
var tipoSolicitacao = NormalizeTipoSolicitacao(req.TipoSolicitacao);
|
||||||
|
if (tipoSolicitacao == null)
|
||||||
|
{
|
||||||
|
return BadRequest(new { message = "Tipo de solicitação inválido. Use 'alteracao-franquia' ou 'bloqueio'." });
|
||||||
|
}
|
||||||
|
|
||||||
|
decimal? franquiaLineNova = null;
|
||||||
|
if (tipoSolicitacao == TipoAlteracaoFranquia)
|
||||||
|
{
|
||||||
|
if (!req.FranquiaLineNova.HasValue)
|
||||||
|
{
|
||||||
|
return BadRequest(new { message = "Informe a nova franquia para solicitar alteração." });
|
||||||
|
}
|
||||||
|
|
||||||
|
franquiaLineNova = decimal.Round(req.FranquiaLineNova.Value, 2, MidpointRounding.AwayFromZero);
|
||||||
|
if (franquiaLineNova < 0)
|
||||||
|
{
|
||||||
|
return BadRequest(new { message = "A nova franquia não pode ser negativa." });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var solicitanteNome = ResolveSolicitanteNome();
|
||||||
|
var usuarioLinha = NormalizeOptionalText(line.Usuario) ?? solicitanteNome;
|
||||||
|
var linha = NormalizeOptionalText(line.Linha) ?? "-";
|
||||||
|
var mensagem = tipoSolicitacao == TipoAlteracaoFranquia
|
||||||
|
? $"O Usuário \"{usuarioLinha}\" solicitou alteração da linha \"{linha}\" \"{FormatFranquia(line.FranquiaLine)}\" -> \"{FormatFranquia(franquiaLineNova)}\""
|
||||||
|
: $"O Usuário \"{usuarioLinha}\" solicitou bloqueio da linha \"{linha}\"";
|
||||||
|
|
||||||
|
var solicitacao = new SolicitacaoLinha
|
||||||
|
{
|
||||||
|
TenantId = line.TenantId,
|
||||||
|
MobileLineId = line.Id,
|
||||||
|
Linha = NormalizeOptionalText(line.Linha),
|
||||||
|
UsuarioLinha = NormalizeOptionalText(line.Usuario),
|
||||||
|
TipoSolicitacao = tipoSolicitacao,
|
||||||
|
FranquiaLineAtual = line.FranquiaLine,
|
||||||
|
FranquiaLineNova = franquiaLineNova,
|
||||||
|
SolicitanteUserId = ResolveSolicitanteUserId(),
|
||||||
|
SolicitanteNome = solicitanteNome,
|
||||||
|
Mensagem = mensagem,
|
||||||
|
Status = "PENDENTE",
|
||||||
|
CreatedAt = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
|
||||||
|
_db.SolicitacaoLinhas.Add(solicitacao);
|
||||||
|
await _db.SaveChangesAsync();
|
||||||
|
|
||||||
|
var tenantNome = await _db.Tenants
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(t => t.Id == solicitacao.TenantId)
|
||||||
|
.Select(t => t.NomeOficial)
|
||||||
|
.FirstOrDefaultAsync();
|
||||||
|
|
||||||
|
return Ok(ToDto(solicitacao, tenantNome));
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
[Authorize(Roles = "sysadmin,gestor")]
|
||||||
|
public async Task<ActionResult<PagedResult<SolicitacaoLinhaListDto>>> List(
|
||||||
|
[FromQuery] string? search,
|
||||||
|
[FromQuery] int page = 1,
|
||||||
|
[FromQuery] int pageSize = 20)
|
||||||
|
{
|
||||||
|
page = page < 1 ? 1 : page;
|
||||||
|
pageSize = pageSize < 1 ? 20 : Math.Min(pageSize, 200);
|
||||||
|
|
||||||
|
var query =
|
||||||
|
from solicitacao in _db.SolicitacaoLinhas.AsNoTracking()
|
||||||
|
join tenant in _db.Tenants.AsNoTracking()
|
||||||
|
on solicitacao.TenantId equals tenant.Id into tenantJoin
|
||||||
|
from tenant in tenantJoin.DefaultIfEmpty()
|
||||||
|
select new
|
||||||
|
{
|
||||||
|
Solicitacao = solicitacao,
|
||||||
|
TenantNome = tenant != null ? tenant.NomeOficial : null
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(search))
|
||||||
|
{
|
||||||
|
var term = search.Trim();
|
||||||
|
query = query.Where(x =>
|
||||||
|
EF.Functions.ILike(x.Solicitacao.Linha ?? "", $"%{term}%") ||
|
||||||
|
EF.Functions.ILike(x.Solicitacao.UsuarioLinha ?? "", $"%{term}%") ||
|
||||||
|
EF.Functions.ILike(x.Solicitacao.SolicitanteNome ?? "", $"%{term}%") ||
|
||||||
|
EF.Functions.ILike(x.Solicitacao.Mensagem ?? "", $"%{term}%"));
|
||||||
|
}
|
||||||
|
|
||||||
|
var total = await query.CountAsync();
|
||||||
|
var items = await query
|
||||||
|
.OrderByDescending(x => x.Solicitacao.CreatedAt)
|
||||||
|
.Skip((page - 1) * pageSize)
|
||||||
|
.Take(pageSize)
|
||||||
|
.Select(x => new SolicitacaoLinhaListDto
|
||||||
|
{
|
||||||
|
Id = x.Solicitacao.Id,
|
||||||
|
TenantId = x.Solicitacao.TenantId,
|
||||||
|
TenantNome = x.TenantNome,
|
||||||
|
MobileLineId = x.Solicitacao.MobileLineId,
|
||||||
|
Linha = x.Solicitacao.Linha,
|
||||||
|
UsuarioLinha = x.Solicitacao.UsuarioLinha,
|
||||||
|
TipoSolicitacao = x.Solicitacao.TipoSolicitacao,
|
||||||
|
FranquiaLineAtual = x.Solicitacao.FranquiaLineAtual,
|
||||||
|
FranquiaLineNova = x.Solicitacao.FranquiaLineNova,
|
||||||
|
SolicitanteNome = x.Solicitacao.SolicitanteNome,
|
||||||
|
Mensagem = x.Solicitacao.Mensagem,
|
||||||
|
Status = x.Solicitacao.Status,
|
||||||
|
CreatedAt = x.Solicitacao.CreatedAt
|
||||||
|
})
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
return Ok(new PagedResult<SolicitacaoLinhaListDto>
|
||||||
|
{
|
||||||
|
Page = page,
|
||||||
|
PageSize = pageSize,
|
||||||
|
Total = total,
|
||||||
|
Items = items
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? NormalizeTipoSolicitacao(string? tipoSolicitacao)
|
||||||
|
{
|
||||||
|
var value = (tipoSolicitacao ?? string.Empty).Trim().ToLowerInvariant();
|
||||||
|
return value switch
|
||||||
|
{
|
||||||
|
"alteracao-franquia" => TipoAlteracaoFranquia,
|
||||||
|
"alteracao_franquia" => TipoAlteracaoFranquia,
|
||||||
|
"alteracaofranquia" => TipoAlteracaoFranquia,
|
||||||
|
"franquia" => TipoAlteracaoFranquia,
|
||||||
|
"bloqueio" => TipoBloqueio,
|
||||||
|
"solicitar-bloqueio" => TipoBloqueio,
|
||||||
|
_ => null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private string ResolveSolicitanteNome()
|
||||||
|
{
|
||||||
|
var fromClaim = User.FindFirstValue("name");
|
||||||
|
if (!string.IsNullOrWhiteSpace(fromClaim))
|
||||||
|
{
|
||||||
|
return fromClaim.Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
var fromIdentity = User.Identity?.Name;
|
||||||
|
if (!string.IsNullOrWhiteSpace(fromIdentity))
|
||||||
|
{
|
||||||
|
return fromIdentity.Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
var fromEmail = User.FindFirstValue(ClaimTypes.Email) ?? User.FindFirstValue("email");
|
||||||
|
if (!string.IsNullOrWhiteSpace(fromEmail))
|
||||||
|
{
|
||||||
|
return fromEmail.Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
return "Usuário";
|
||||||
|
}
|
||||||
|
|
||||||
|
private Guid? ResolveSolicitanteUserId()
|
||||||
|
{
|
||||||
|
var raw =
|
||||||
|
User.FindFirstValue(ClaimTypes.NameIdentifier) ??
|
||||||
|
User.FindFirstValue("sub");
|
||||||
|
|
||||||
|
return Guid.TryParse(raw, out var parsed) ? parsed : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? NormalizeOptionalText(string? value)
|
||||||
|
{
|
||||||
|
return string.IsNullOrWhiteSpace(value) ? null : value.Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string FormatFranquia(decimal? value)
|
||||||
|
{
|
||||||
|
return value.HasValue
|
||||||
|
? value.Value.ToString("0.##", CultureInfo.GetCultureInfo("pt-BR"))
|
||||||
|
: "-";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SolicitacaoLinhaListDto ToDto(SolicitacaoLinha solicitacao, string? tenantNome)
|
||||||
|
{
|
||||||
|
return new SolicitacaoLinhaListDto
|
||||||
|
{
|
||||||
|
Id = solicitacao.Id,
|
||||||
|
TenantId = solicitacao.TenantId,
|
||||||
|
TenantNome = tenantNome,
|
||||||
|
MobileLineId = solicitacao.MobileLineId,
|
||||||
|
Linha = solicitacao.Linha,
|
||||||
|
UsuarioLinha = solicitacao.UsuarioLinha,
|
||||||
|
TipoSolicitacao = solicitacao.TipoSolicitacao,
|
||||||
|
FranquiaLineAtual = solicitacao.FranquiaLineAtual,
|
||||||
|
FranquiaLineNova = solicitacao.FranquiaLineNova,
|
||||||
|
SolicitanteNome = solicitacao.SolicitanteNome,
|
||||||
|
Mensagem = solicitacao.Mensagem,
|
||||||
|
Status = solicitacao.Status,
|
||||||
|
CreatedAt = solicitacao.CreatedAt
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -6,7 +6,7 @@ namespace line_gestao_api.Controllers
|
||||||
{
|
{
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("api/templates")]
|
[Route("api/templates")]
|
||||||
[Authorize(Roles = "sysadmin,gestor")]
|
[Authorize(Roles = "sysadmin,gestor,financeiro")]
|
||||||
public class TemplatesController : ControllerBase
|
public class TemplatesController : ControllerBase
|
||||||
{
|
{
|
||||||
private readonly GeralSpreadsheetTemplateService _geralSpreadsheetTemplateService;
|
private readonly GeralSpreadsheetTemplateService _geralSpreadsheetTemplateService;
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ public class UsersController : ControllerBase
|
||||||
{
|
{
|
||||||
AppRoles.SysAdmin,
|
AppRoles.SysAdmin,
|
||||||
AppRoles.Gestor,
|
AppRoles.Gestor,
|
||||||
|
AppRoles.Financeiro,
|
||||||
AppRoles.Cliente
|
AppRoles.Cliente
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,8 @@ public class AppDbContext : IdentityDbContext<ApplicationUser, IdentityRole<Guid
|
||||||
|
|
||||||
// ✅ tabela para espelhar a planilha (GERAL)
|
// ✅ tabela para espelhar a planilha (GERAL)
|
||||||
public DbSet<MobileLine> MobileLines => Set<MobileLine>();
|
public DbSet<MobileLine> MobileLines => Set<MobileLine>();
|
||||||
|
public DbSet<Setor> Setores => Set<Setor>();
|
||||||
|
public DbSet<Aparelho> Aparelhos => Set<Aparelho>();
|
||||||
|
|
||||||
// ✅ tabela para espelhar a aba MUREG
|
// ✅ tabela para espelhar a aba MUREG
|
||||||
public DbSet<MuregLine> MuregLines => Set<MuregLine>();
|
public DbSet<MuregLine> MuregLines => Set<MuregLine>();
|
||||||
|
|
@ -48,6 +50,7 @@ public class AppDbContext : IdentityDbContext<ApplicationUser, IdentityRole<Guid
|
||||||
|
|
||||||
// ✅ tabela NOTIFICAÇÕES
|
// ✅ tabela NOTIFICAÇÕES
|
||||||
public DbSet<Notification> Notifications => Set<Notification>();
|
public DbSet<Notification> Notifications => Set<Notification>();
|
||||||
|
public DbSet<SolicitacaoLinha> SolicitacaoLinhas => Set<SolicitacaoLinha>();
|
||||||
|
|
||||||
// ✅ tabela RESUMO
|
// ✅ tabela RESUMO
|
||||||
public DbSet<ResumoMacrophonyPlan> ResumoMacrophonyPlans => Set<ResumoMacrophonyPlan>();
|
public DbSet<ResumoMacrophonyPlan> ResumoMacrophonyPlans => Set<ResumoMacrophonyPlan>();
|
||||||
|
|
@ -74,6 +77,10 @@ public class AppDbContext : IdentityDbContext<ApplicationUser, IdentityRole<Guid
|
||||||
public DbSet<ImportAuditRun> ImportAuditRuns => Set<ImportAuditRun>();
|
public DbSet<ImportAuditRun> ImportAuditRuns => Set<ImportAuditRun>();
|
||||||
public DbSet<ImportAuditIssue> ImportAuditIssues => Set<ImportAuditIssue>();
|
public DbSet<ImportAuditIssue> ImportAuditIssues => Set<ImportAuditIssue>();
|
||||||
|
|
||||||
|
// ✅ tabelas de auditoria MVE
|
||||||
|
public DbSet<MveAuditRun> MveAuditRuns => Set<MveAuditRun>();
|
||||||
|
public DbSet<MveAuditIssue> MveAuditIssues => Set<MveAuditIssue>();
|
||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
base.OnModelCreating(modelBuilder);
|
base.OnModelCreating(modelBuilder);
|
||||||
|
|
@ -87,6 +94,27 @@ public class AppDbContext : IdentityDbContext<ApplicationUser, IdentityRole<Guid
|
||||||
e.HasIndex(x => new { x.IsSystem, x.Ativo });
|
e.HasIndex(x => new { x.IsSystem, x.Ativo });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity<Setor>(e =>
|
||||||
|
{
|
||||||
|
e.Property(x => x.Nome).HasMaxLength(160);
|
||||||
|
e.HasIndex(x => x.TenantId);
|
||||||
|
e.HasIndex(x => new { x.TenantId, x.Nome }).IsUnique();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity<Aparelho>(e =>
|
||||||
|
{
|
||||||
|
e.Property(x => x.Nome).HasMaxLength(160);
|
||||||
|
e.Property(x => x.Fabricante).HasMaxLength(120);
|
||||||
|
e.Property(x => x.Cor).HasMaxLength(80);
|
||||||
|
e.Property(x => x.Imei).HasMaxLength(80);
|
||||||
|
e.Property(x => x.NotaFiscalArquivoPath).HasMaxLength(500);
|
||||||
|
e.Property(x => x.ReciboArquivoPath).HasMaxLength(500);
|
||||||
|
e.HasIndex(x => x.TenantId);
|
||||||
|
e.HasIndex(x => x.Imei);
|
||||||
|
e.HasIndex(x => new { x.TenantId, x.Nome, x.Cor });
|
||||||
|
e.HasIndex(x => new { x.TenantId, x.Fabricante });
|
||||||
|
});
|
||||||
|
|
||||||
// =========================
|
// =========================
|
||||||
// ✅ USER (Identity)
|
// ✅ USER (Identity)
|
||||||
// =========================
|
// =========================
|
||||||
|
|
@ -104,13 +132,27 @@ public class AppDbContext : IdentityDbContext<ApplicationUser, IdentityRole<Guid
|
||||||
{
|
{
|
||||||
// Mantém UNIQUE por Linha por tenant (se Linha puder ser null no banco, Postgres aceita múltiplos nulls)
|
// Mantém UNIQUE por Linha por tenant (se Linha puder ser null no banco, Postgres aceita múltiplos nulls)
|
||||||
e.HasIndex(x => new { x.TenantId, x.Linha }).IsUnique();
|
e.HasIndex(x => new { x.TenantId, x.Linha }).IsUnique();
|
||||||
|
e.Property(x => x.CentroDeCustos).HasMaxLength(180);
|
||||||
|
|
||||||
// performance
|
// performance
|
||||||
e.HasIndex(x => x.Chip);
|
e.HasIndex(x => x.Chip);
|
||||||
e.HasIndex(x => x.Cliente);
|
e.HasIndex(x => x.Cliente);
|
||||||
e.HasIndex(x => x.Usuario);
|
e.HasIndex(x => x.Usuario);
|
||||||
|
e.HasIndex(x => x.CentroDeCustos);
|
||||||
e.HasIndex(x => x.Skil);
|
e.HasIndex(x => x.Skil);
|
||||||
e.HasIndex(x => x.Status);
|
e.HasIndex(x => x.Status);
|
||||||
|
e.HasIndex(x => x.SetorId);
|
||||||
|
e.HasIndex(x => x.AparelhoId);
|
||||||
|
|
||||||
|
e.HasOne(x => x.Setor)
|
||||||
|
.WithMany(x => x.MobileLines)
|
||||||
|
.HasForeignKey(x => x.SetorId)
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
|
||||||
|
e.HasOne(x => x.Aparelho)
|
||||||
|
.WithMany(x => x.MobileLines)
|
||||||
|
.HasForeignKey(x => x.AparelhoId)
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
});
|
});
|
||||||
|
|
||||||
// =========================
|
// =========================
|
||||||
|
|
@ -246,6 +288,26 @@ public class AppDbContext : IdentityDbContext<ApplicationUser, IdentityRole<Guid
|
||||||
.OnDelete(DeleteBehavior.Restrict);
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity<SolicitacaoLinha>(e =>
|
||||||
|
{
|
||||||
|
e.HasIndex(x => x.TenantId);
|
||||||
|
e.HasIndex(x => x.CreatedAt);
|
||||||
|
e.HasIndex(x => x.TipoSolicitacao);
|
||||||
|
e.HasIndex(x => x.Status);
|
||||||
|
e.HasIndex(x => x.MobileLineId);
|
||||||
|
e.HasIndex(x => x.SolicitanteUserId);
|
||||||
|
|
||||||
|
e.HasOne(x => x.MobileLine)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(x => x.MobileLineId)
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
|
||||||
|
e.HasOne(x => x.SolicitanteUser)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(x => x.SolicitanteUserId)
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
});
|
||||||
|
|
||||||
// =========================
|
// =========================
|
||||||
// ✅ PARCELAMENTOS
|
// ✅ PARCELAMENTOS
|
||||||
// =========================
|
// =========================
|
||||||
|
|
@ -334,8 +396,52 @@ public class AppDbContext : IdentityDbContext<ApplicationUser, IdentityRole<Guid
|
||||||
.OnDelete(DeleteBehavior.Cascade);
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity<MveAuditRun>(e =>
|
||||||
|
{
|
||||||
|
e.Property(x => x.FileName).HasMaxLength(260);
|
||||||
|
e.Property(x => x.FileHashSha256).HasMaxLength(64);
|
||||||
|
e.Property(x => x.FileEncoding).HasMaxLength(40);
|
||||||
|
e.Property(x => x.Status).HasMaxLength(40);
|
||||||
|
e.Property(x => x.AppliedByUserName).HasMaxLength(200);
|
||||||
|
e.Property(x => x.AppliedByUserEmail).HasMaxLength(200);
|
||||||
|
|
||||||
|
e.HasIndex(x => x.TenantId);
|
||||||
|
e.HasIndex(x => x.ImportedAtUtc);
|
||||||
|
e.HasIndex(x => x.Status);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity<MveAuditIssue>(e =>
|
||||||
|
{
|
||||||
|
e.Property(x => x.NumeroLinha).HasMaxLength(64);
|
||||||
|
e.Property(x => x.IssueType).HasMaxLength(60);
|
||||||
|
e.Property(x => x.Situation).HasMaxLength(80);
|
||||||
|
e.Property(x => x.Severity).HasMaxLength(40);
|
||||||
|
e.Property(x => x.ActionSuggestion).HasMaxLength(160);
|
||||||
|
e.Property(x => x.Notes).HasMaxLength(500);
|
||||||
|
e.Property(x => x.SystemStatus).HasMaxLength(120);
|
||||||
|
e.Property(x => x.ReportStatus).HasMaxLength(120);
|
||||||
|
e.Property(x => x.SystemPlan).HasMaxLength(200);
|
||||||
|
e.Property(x => x.ReportPlan).HasMaxLength(200);
|
||||||
|
e.Property(x => x.SystemSnapshotJson).HasColumnType("jsonb");
|
||||||
|
e.Property(x => x.ReportSnapshotJson).HasColumnType("jsonb");
|
||||||
|
e.Property(x => x.DifferencesJson).HasColumnType("jsonb");
|
||||||
|
|
||||||
|
e.HasIndex(x => x.TenantId);
|
||||||
|
e.HasIndex(x => x.AuditRunId);
|
||||||
|
e.HasIndex(x => x.IssueType);
|
||||||
|
e.HasIndex(x => x.NumeroLinha);
|
||||||
|
e.HasIndex(x => x.Syncable);
|
||||||
|
|
||||||
|
e.HasOne(x => x.AuditRun)
|
||||||
|
.WithMany(x => x.Issues)
|
||||||
|
.HasForeignKey(x => x.AuditRunId)
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity<MobileLine>().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId));
|
modelBuilder.Entity<MobileLine>().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId));
|
||||||
modelBuilder.Entity<MuregLine>().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId));
|
modelBuilder.Entity<MuregLine>().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId));
|
||||||
|
modelBuilder.Entity<Setor>().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId));
|
||||||
|
modelBuilder.Entity<Aparelho>().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId));
|
||||||
modelBuilder.Entity<BillingClient>().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId));
|
modelBuilder.Entity<BillingClient>().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId));
|
||||||
modelBuilder.Entity<UserData>().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId));
|
modelBuilder.Entity<UserData>().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId));
|
||||||
modelBuilder.Entity<VigenciaLine>().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId));
|
modelBuilder.Entity<VigenciaLine>().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId));
|
||||||
|
|
@ -343,6 +449,7 @@ public class AppDbContext : IdentityDbContext<ApplicationUser, IdentityRole<Guid
|
||||||
modelBuilder.Entity<ChipVirgemLine>().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId));
|
modelBuilder.Entity<ChipVirgemLine>().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId));
|
||||||
modelBuilder.Entity<ControleRecebidoLine>().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId));
|
modelBuilder.Entity<ControleRecebidoLine>().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId));
|
||||||
modelBuilder.Entity<Notification>().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId));
|
modelBuilder.Entity<Notification>().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId));
|
||||||
|
modelBuilder.Entity<SolicitacaoLinha>().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId));
|
||||||
modelBuilder.Entity<ResumoMacrophonyPlan>().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId));
|
modelBuilder.Entity<ResumoMacrophonyPlan>().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId));
|
||||||
modelBuilder.Entity<ResumoMacrophonyTotal>().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId));
|
modelBuilder.Entity<ResumoMacrophonyTotal>().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId));
|
||||||
modelBuilder.Entity<ResumoVivoLineResumo>().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId));
|
modelBuilder.Entity<ResumoVivoLineResumo>().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId));
|
||||||
|
|
@ -360,6 +467,8 @@ public class AppDbContext : IdentityDbContext<ApplicationUser, IdentityRole<Guid
|
||||||
modelBuilder.Entity<AuditLog>().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId));
|
modelBuilder.Entity<AuditLog>().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId));
|
||||||
modelBuilder.Entity<ImportAuditRun>().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId));
|
modelBuilder.Entity<ImportAuditRun>().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId));
|
||||||
modelBuilder.Entity<ImportAuditIssue>().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId));
|
modelBuilder.Entity<ImportAuditIssue>().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId));
|
||||||
|
modelBuilder.Entity<MveAuditRun>().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId));
|
||||||
|
modelBuilder.Entity<MveAuditIssue>().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId));
|
||||||
modelBuilder.Entity<ApplicationUser>().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId));
|
modelBuilder.Entity<ApplicationUser>().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,13 @@ namespace line_gestao_api.Dtos
|
||||||
public string? Chip { get; set; } // ICCID
|
public string? Chip { get; set; } // ICCID
|
||||||
public string? Cliente { get; set; } // Obrigatório na validação do Controller
|
public string? Cliente { get; set; } // Obrigatório na validação do Controller
|
||||||
public string? Usuario { get; set; }
|
public string? Usuario { get; set; }
|
||||||
|
public string? CentroDeCustos { get; set; }
|
||||||
|
public Guid? SetorId { get; set; }
|
||||||
|
public string? SetorNome { get; set; }
|
||||||
|
public Guid? AparelhoId { get; set; }
|
||||||
|
public string? AparelhoNome { get; set; }
|
||||||
|
public string? AparelhoCor { get; set; }
|
||||||
|
public string? AparelhoImei { get; set; }
|
||||||
public Guid? ReservaLineId { get; set; } // Reaproveita linha já existente na Reserva
|
public Guid? ReservaLineId { get; set; } // Reaproveita linha já existente na Reserva
|
||||||
|
|
||||||
// ==========================
|
// ==========================
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ namespace line_gestao_api.Dtos
|
||||||
{
|
{
|
||||||
public int QtdLinhas { get; set; }
|
public int QtdLinhas { get; set; }
|
||||||
public decimal TotalFranquiaGb { get; set; }
|
public decimal TotalFranquiaGb { get; set; }
|
||||||
|
public decimal TotalFranquiaLine { get; set; }
|
||||||
public decimal TotalBaseMensal { get; set; }
|
public decimal TotalBaseMensal { get; set; }
|
||||||
public decimal TotalAdicionaisMensal { get; set; }
|
public decimal TotalAdicionaisMensal { get; set; }
|
||||||
public decimal TotalGeralMensal { get; set; }
|
public decimal TotalGeralMensal { get; set; }
|
||||||
|
|
|
||||||
|
|
@ -71,4 +71,42 @@ namespace line_gestao_api.Dtos
|
||||||
public bool Success { get; set; }
|
public bool Success { get; set; }
|
||||||
public string Message { get; set; } = string.Empty;
|
public string Message { get; set; } = string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public sealed class BatchLineStatusUpdateRequestDto
|
||||||
|
{
|
||||||
|
// "block" | "unblock"
|
||||||
|
public string? Action { get; set; }
|
||||||
|
public string? BlockStatus { get; set; }
|
||||||
|
public bool ApplyToAllFiltered { get; set; }
|
||||||
|
|
||||||
|
public List<Guid> LineIds { get; set; } = new();
|
||||||
|
|
||||||
|
// Filtros da tela Geral
|
||||||
|
public string? Search { get; set; }
|
||||||
|
public string? Skil { get; set; }
|
||||||
|
public List<string> Clients { get; set; } = new();
|
||||||
|
public string? AdditionalMode { get; set; }
|
||||||
|
public string? AdditionalServices { get; set; }
|
||||||
|
public string? Usuario { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class BatchLineStatusUpdateResultDto
|
||||||
|
{
|
||||||
|
public int Requested { get; set; }
|
||||||
|
public int Updated { get; set; }
|
||||||
|
public int Failed { get; set; }
|
||||||
|
public List<BatchLineStatusUpdateItemResultDto> Items { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class BatchLineStatusUpdateItemResultDto
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
public int Item { get; set; }
|
||||||
|
public string? Linha { get; set; }
|
||||||
|
public string? Usuario { get; set; }
|
||||||
|
public string? StatusAnterior { get; set; }
|
||||||
|
public string? StatusNovo { get; set; }
|
||||||
|
public bool Success { get; set; }
|
||||||
|
public string Message { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,11 +9,18 @@
|
||||||
public string? Chip { get; set; }
|
public string? Chip { get; set; }
|
||||||
public string? Cliente { get; set; }
|
public string? Cliente { get; set; }
|
||||||
public string? Usuario { get; set; }
|
public string? Usuario { get; set; }
|
||||||
|
public string? CentroDeCustos { get; set; }
|
||||||
|
public string? SetorNome { get; set; }
|
||||||
|
public string? AparelhoNome { get; set; }
|
||||||
|
public string? AparelhoCor { get; set; }
|
||||||
public string? PlanoContrato { get; set; }
|
public string? PlanoContrato { get; set; }
|
||||||
public string? Status { get; set; }
|
public string? Status { get; set; }
|
||||||
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 string? ContaEmpresa { get; set; }
|
||||||
|
public string? Operadora { get; set; }
|
||||||
|
public decimal? FranquiaVivo { get; set; }
|
||||||
public decimal? FranquiaLine { get; set; }
|
public decimal? FranquiaLine { get; set; }
|
||||||
|
|
||||||
// Campos para filtro deterministico de adicionais no frontend
|
// Campos para filtro deterministico de adicionais no frontend
|
||||||
|
|
@ -30,11 +37,21 @@
|
||||||
{
|
{
|
||||||
public Guid Id { get; set; }
|
public Guid Id { get; set; }
|
||||||
public int Item { get; set; }
|
public int Item { get; set; }
|
||||||
|
public string? ContaEmpresa { get; set; }
|
||||||
public string? Conta { get; set; }
|
public string? Conta { get; set; }
|
||||||
public string? Linha { get; set; }
|
public string? Linha { get; set; }
|
||||||
public string? Chip { get; set; }
|
public string? Chip { get; set; }
|
||||||
public string? Cliente { get; set; }
|
public string? Cliente { get; set; }
|
||||||
public string? Usuario { get; set; }
|
public string? Usuario { get; set; }
|
||||||
|
public string? CentroDeCustos { get; set; }
|
||||||
|
public Guid? SetorId { get; set; }
|
||||||
|
public string? SetorNome { get; set; }
|
||||||
|
public Guid? AparelhoId { get; set; }
|
||||||
|
public string? AparelhoNome { get; set; }
|
||||||
|
public string? AparelhoCor { get; set; }
|
||||||
|
public string? AparelhoImei { get; set; }
|
||||||
|
public bool AparelhoNotaFiscalTemArquivo { get; set; }
|
||||||
|
public bool AparelhoReciboTemArquivo { get; set; }
|
||||||
public string? PlanoContrato { get; set; }
|
public string? PlanoContrato { get; set; }
|
||||||
|
|
||||||
public decimal? FranquiaVivo { get; set; }
|
public decimal? FranquiaVivo { get; set; }
|
||||||
|
|
@ -78,6 +95,13 @@
|
||||||
public string? Chip { get; set; }
|
public string? Chip { get; set; }
|
||||||
public string? Cliente { get; set; }
|
public string? Cliente { get; set; }
|
||||||
public string? Usuario { get; set; }
|
public string? Usuario { get; set; }
|
||||||
|
public string? CentroDeCustos { get; set; }
|
||||||
|
public Guid? SetorId { get; set; }
|
||||||
|
public string? SetorNome { get; set; }
|
||||||
|
public Guid? AparelhoId { get; set; }
|
||||||
|
public string? AparelhoNome { get; set; }
|
||||||
|
public string? AparelhoCor { get; set; }
|
||||||
|
public string? AparelhoImei { get; set; }
|
||||||
public string? PlanoContrato { get; set; }
|
public string? PlanoContrato { get; set; }
|
||||||
|
|
||||||
public decimal? FranquiaVivo { get; set; }
|
public decimal? FranquiaVivo { get; set; }
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,158 @@
|
||||||
|
namespace line_gestao_api.Dtos;
|
||||||
|
|
||||||
|
public class MveAuditRunDto
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
|
||||||
|
public string? FileName { get; set; }
|
||||||
|
|
||||||
|
public string? FileEncoding { get; set; }
|
||||||
|
|
||||||
|
public string Status { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public DateTime ImportedAtUtc { get; set; }
|
||||||
|
|
||||||
|
public DateTime? AppliedAtUtc { get; set; }
|
||||||
|
|
||||||
|
public string? AppliedByUserName { get; set; }
|
||||||
|
|
||||||
|
public string? AppliedByUserEmail { get; set; }
|
||||||
|
|
||||||
|
public MveAuditSummaryDto Summary { get; set; } = new();
|
||||||
|
|
||||||
|
public List<MveAuditIssueDto> Issues { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class MveAuditSummaryDto
|
||||||
|
{
|
||||||
|
public int TotalSystemLines { get; set; }
|
||||||
|
|
||||||
|
public int TotalReportLines { get; set; }
|
||||||
|
|
||||||
|
public int TotalConciliated { get; set; }
|
||||||
|
|
||||||
|
public int TotalStatusDivergences { get; set; }
|
||||||
|
|
||||||
|
public int TotalDataDivergences { get; set; }
|
||||||
|
|
||||||
|
public int TotalOnlyInSystem { get; set; }
|
||||||
|
|
||||||
|
public int TotalOnlyInReport { get; set; }
|
||||||
|
|
||||||
|
public int TotalDuplicateReportLines { get; set; }
|
||||||
|
|
||||||
|
public int TotalDuplicateSystemLines { get; set; }
|
||||||
|
|
||||||
|
public int TotalInvalidRows { get; set; }
|
||||||
|
|
||||||
|
public int TotalUnknownStatuses { get; set; }
|
||||||
|
|
||||||
|
public int TotalSyncableIssues { get; set; }
|
||||||
|
|
||||||
|
public int AppliedIssuesCount { get; set; }
|
||||||
|
|
||||||
|
public int AppliedLinesCount { get; set; }
|
||||||
|
|
||||||
|
public int AppliedFieldsCount { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class MveAuditIssueDto
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
|
||||||
|
public int? SourceRowNumber { get; set; }
|
||||||
|
|
||||||
|
public string NumeroLinha { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public Guid? MobileLineId { get; set; }
|
||||||
|
|
||||||
|
public int? SystemItem { get; set; }
|
||||||
|
|
||||||
|
public string IssueType { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string Situation { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string Severity { get; set; } = "INFO";
|
||||||
|
|
||||||
|
public bool Syncable { get; set; }
|
||||||
|
|
||||||
|
public bool Applied { get; set; }
|
||||||
|
|
||||||
|
public string? ActionSuggestion { get; set; }
|
||||||
|
|
||||||
|
public string? Notes { get; set; }
|
||||||
|
|
||||||
|
public string? SystemStatus { get; set; }
|
||||||
|
|
||||||
|
public string? ReportStatus { get; set; }
|
||||||
|
|
||||||
|
public string? SystemPlan { get; set; }
|
||||||
|
|
||||||
|
public string? ReportPlan { get; set; }
|
||||||
|
|
||||||
|
public MveAuditSnapshotDto? SystemSnapshot { get; set; }
|
||||||
|
|
||||||
|
public MveAuditSnapshotDto? ReportSnapshot { get; set; }
|
||||||
|
|
||||||
|
public List<MveAuditDifferenceDto> Differences { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class MveAuditSnapshotDto
|
||||||
|
{
|
||||||
|
public string? NumeroLinha { get; set; }
|
||||||
|
|
||||||
|
public string? StatusLinha { get; set; }
|
||||||
|
|
||||||
|
public string? StatusConta { get; set; }
|
||||||
|
|
||||||
|
public string? PlanoLinha { get; set; }
|
||||||
|
|
||||||
|
public DateTime? DataAtivacao { get; set; }
|
||||||
|
|
||||||
|
public DateTime? TerminoContrato { get; set; }
|
||||||
|
|
||||||
|
public string? Chip { get; set; }
|
||||||
|
|
||||||
|
public string? Conta { get; set; }
|
||||||
|
|
||||||
|
public string? Cnpj { get; set; }
|
||||||
|
|
||||||
|
public string? ModeloAparelho { get; set; }
|
||||||
|
|
||||||
|
public string? Fabricante { get; set; }
|
||||||
|
|
||||||
|
public List<string> ServicosAtivos { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class MveAuditDifferenceDto
|
||||||
|
{
|
||||||
|
public string FieldKey { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string Label { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string? SystemValue { get; set; }
|
||||||
|
|
||||||
|
public string? ReportValue { get; set; }
|
||||||
|
|
||||||
|
public bool Syncable { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ApplyMveAuditRequestDto
|
||||||
|
{
|
||||||
|
public List<Guid>? IssueIds { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ApplyMveAuditResultDto
|
||||||
|
{
|
||||||
|
public Guid AuditRunId { get; set; }
|
||||||
|
|
||||||
|
public int RequestedIssues { get; set; }
|
||||||
|
|
||||||
|
public int AppliedIssues { get; set; }
|
||||||
|
|
||||||
|
public int UpdatedLines { get; set; }
|
||||||
|
|
||||||
|
public int UpdatedFields { get; set; }
|
||||||
|
|
||||||
|
public int SkippedIssues { get; set; }
|
||||||
|
}
|
||||||
|
|
@ -6,6 +6,7 @@ namespace line_gestao_api.Dtos
|
||||||
public class RelatoriosDashboardDto
|
public class RelatoriosDashboardDto
|
||||||
{
|
{
|
||||||
public DashboardKpisDto Kpis { get; set; } = new();
|
public DashboardKpisDto Kpis { get; set; } = new();
|
||||||
|
public Dictionary<string, string> KpiTrends { get; set; } = new();
|
||||||
public List<TopClienteDto> TopClientes { get; set; } = new();
|
public List<TopClienteDto> TopClientes { get; set; } = new();
|
||||||
public List<SerieMesDto> SerieMuregUltimos12Meses { get; set; } = new();
|
public List<SerieMesDto> SerieMuregUltimos12Meses { get; set; } = new();
|
||||||
public List<SerieMesDto> SerieTrocaUltimos12Meses { get; set; } = new();
|
public List<SerieMesDto> SerieTrocaUltimos12Meses { get; set; } = new();
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
namespace line_gestao_api.Dtos;
|
||||||
|
|
||||||
|
public class CreateSolicitacaoLinhaRequestDto
|
||||||
|
{
|
||||||
|
public Guid LineId { get; set; }
|
||||||
|
public string TipoSolicitacao { get; set; } = string.Empty;
|
||||||
|
public decimal? FranquiaLineNova { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SolicitacaoLinhaListDto
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
public Guid TenantId { get; set; }
|
||||||
|
public string? TenantNome { get; set; }
|
||||||
|
public Guid? MobileLineId { get; set; }
|
||||||
|
public string? Linha { get; set; }
|
||||||
|
public string? UsuarioLinha { get; set; }
|
||||||
|
public string TipoSolicitacao { get; set; } = string.Empty;
|
||||||
|
public decimal? FranquiaLineAtual { get; set; }
|
||||||
|
public decimal? FranquiaLineNova { get; set; }
|
||||||
|
public string? SolicitanteNome { get; set; }
|
||||||
|
public string Mensagem { get; set; } = string.Empty;
|
||||||
|
public string Status { get; set; } = string.Empty;
|
||||||
|
public DateTime CreatedAt { get; set; }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,114 @@
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using line_gestao_api.Data;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace line_gestao_api.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(AppDbContext))]
|
||||||
|
[Migration("20260303120000_AddSetoresAparelhosAndMobileLineCostCenter")]
|
||||||
|
public partial class AddSetoresAparelhosAndMobileLineCostCenter : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.Sql("""
|
||||||
|
CREATE TABLE IF NOT EXISTS "Setores" (
|
||||||
|
"Id" uuid NOT NULL,
|
||||||
|
"TenantId" uuid NOT NULL,
|
||||||
|
"Nome" character varying(160) NOT NULL,
|
||||||
|
"CreatedAt" timestamp with time zone NOT NULL,
|
||||||
|
"UpdatedAt" timestamp with time zone NOT NULL,
|
||||||
|
CONSTRAINT "PK_Setores" PRIMARY KEY ("Id")
|
||||||
|
);
|
||||||
|
""");
|
||||||
|
|
||||||
|
migrationBuilder.Sql("""
|
||||||
|
CREATE TABLE IF NOT EXISTS "Aparelhos" (
|
||||||
|
"Id" uuid NOT NULL,
|
||||||
|
"TenantId" uuid NOT NULL,
|
||||||
|
"Nome" character varying(160) NULL,
|
||||||
|
"Cor" character varying(80) NULL,
|
||||||
|
"Imei" character varying(80) NULL,
|
||||||
|
"NotaFiscalArquivoPath" character varying(500) NULL,
|
||||||
|
"ReciboArquivoPath" character varying(500) NULL,
|
||||||
|
"CreatedAt" timestamp with time zone NOT NULL,
|
||||||
|
"UpdatedAt" timestamp with time zone NOT NULL,
|
||||||
|
CONSTRAINT "PK_Aparelhos" PRIMARY KEY ("Id")
|
||||||
|
);
|
||||||
|
""");
|
||||||
|
|
||||||
|
migrationBuilder.Sql("""ALTER TABLE "MobileLines" ADD COLUMN IF NOT EXISTS "CentroDeCustos" character varying(180) NULL;""");
|
||||||
|
migrationBuilder.Sql("""ALTER TABLE "MobileLines" ADD COLUMN IF NOT EXISTS "SetorId" uuid NULL;""");
|
||||||
|
migrationBuilder.Sql("""ALTER TABLE "MobileLines" ADD COLUMN IF NOT EXISTS "AparelhoId" uuid NULL;""");
|
||||||
|
|
||||||
|
migrationBuilder.Sql("""CREATE INDEX IF NOT EXISTS "IX_Setores_TenantId" ON "Setores" ("TenantId");""");
|
||||||
|
migrationBuilder.Sql("""CREATE UNIQUE INDEX IF NOT EXISTS "IX_Setores_TenantId_Nome" ON "Setores" ("TenantId", "Nome");""");
|
||||||
|
migrationBuilder.Sql("""CREATE INDEX IF NOT EXISTS "IX_Aparelhos_TenantId" ON "Aparelhos" ("TenantId");""");
|
||||||
|
migrationBuilder.Sql("""CREATE INDEX IF NOT EXISTS "IX_Aparelhos_Imei" ON "Aparelhos" ("Imei");""");
|
||||||
|
migrationBuilder.Sql("""CREATE INDEX IF NOT EXISTS "IX_Aparelhos_TenantId_Nome_Cor" ON "Aparelhos" ("TenantId", "Nome", "Cor");""");
|
||||||
|
migrationBuilder.Sql("""CREATE INDEX IF NOT EXISTS "IX_MobileLines_CentroDeCustos" ON "MobileLines" ("CentroDeCustos");""");
|
||||||
|
migrationBuilder.Sql("""CREATE INDEX IF NOT EXISTS "IX_MobileLines_SetorId" ON "MobileLines" ("SetorId");""");
|
||||||
|
migrationBuilder.Sql("""CREATE INDEX IF NOT EXISTS "IX_MobileLines_AparelhoId" ON "MobileLines" ("AparelhoId");""");
|
||||||
|
|
||||||
|
migrationBuilder.Sql("""
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM information_schema.table_constraints
|
||||||
|
WHERE constraint_name = 'FK_MobileLines_Setores_SetorId'
|
||||||
|
AND table_name = 'MobileLines'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE "MobileLines"
|
||||||
|
ADD CONSTRAINT "FK_MobileLines_Setores_SetorId"
|
||||||
|
FOREIGN KEY ("SetorId") REFERENCES "Setores" ("Id")
|
||||||
|
ON DELETE SET NULL;
|
||||||
|
END IF;
|
||||||
|
END
|
||||||
|
$$;
|
||||||
|
""");
|
||||||
|
|
||||||
|
migrationBuilder.Sql("""
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM information_schema.table_constraints
|
||||||
|
WHERE constraint_name = 'FK_MobileLines_Aparelhos_AparelhoId'
|
||||||
|
AND table_name = 'MobileLines'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE "MobileLines"
|
||||||
|
ADD CONSTRAINT "FK_MobileLines_Aparelhos_AparelhoId"
|
||||||
|
FOREIGN KEY ("AparelhoId") REFERENCES "Aparelhos" ("Id")
|
||||||
|
ON DELETE SET NULL;
|
||||||
|
END IF;
|
||||||
|
END
|
||||||
|
$$;
|
||||||
|
""");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.Sql("""ALTER TABLE "MobileLines" DROP CONSTRAINT IF EXISTS "FK_MobileLines_Setores_SetorId";""");
|
||||||
|
migrationBuilder.Sql("""ALTER TABLE "MobileLines" DROP CONSTRAINT IF EXISTS "FK_MobileLines_Aparelhos_AparelhoId";""");
|
||||||
|
|
||||||
|
migrationBuilder.Sql("""DROP INDEX IF EXISTS "IX_MobileLines_AparelhoId";""");
|
||||||
|
migrationBuilder.Sql("""DROP INDEX IF EXISTS "IX_MobileLines_SetorId";""");
|
||||||
|
migrationBuilder.Sql("""DROP INDEX IF EXISTS "IX_MobileLines_CentroDeCustos";""");
|
||||||
|
|
||||||
|
migrationBuilder.Sql("""ALTER TABLE "MobileLines" DROP COLUMN IF EXISTS "AparelhoId";""");
|
||||||
|
migrationBuilder.Sql("""ALTER TABLE "MobileLines" DROP COLUMN IF EXISTS "SetorId";""");
|
||||||
|
migrationBuilder.Sql("""ALTER TABLE "MobileLines" DROP COLUMN IF EXISTS "CentroDeCustos";""");
|
||||||
|
|
||||||
|
migrationBuilder.Sql("""DROP INDEX IF EXISTS "IX_Aparelhos_TenantId_Nome_Cor";""");
|
||||||
|
migrationBuilder.Sql("""DROP INDEX IF EXISTS "IX_Aparelhos_Imei";""");
|
||||||
|
migrationBuilder.Sql("""DROP INDEX IF EXISTS "IX_Aparelhos_TenantId";""");
|
||||||
|
migrationBuilder.Sql("""DROP INDEX IF EXISTS "IX_Setores_TenantId_Nome";""");
|
||||||
|
migrationBuilder.Sql("""DROP INDEX IF EXISTS "IX_Setores_TenantId";""");
|
||||||
|
|
||||||
|
migrationBuilder.Sql("""DROP TABLE IF EXISTS "Aparelhos";""");
|
||||||
|
migrationBuilder.Sql("""DROP TABLE IF EXISTS "Setores";""");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using line_gestao_api.Data;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace line_gestao_api.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(AppDbContext))]
|
||||||
|
[Migration("20260303193000_FixAparelhosArquivoPathColumns")]
|
||||||
|
public partial class FixAparelhosArquivoPathColumns : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.Sql("""
|
||||||
|
ALTER TABLE "Aparelhos"
|
||||||
|
ADD COLUMN IF NOT EXISTS "NotaFiscalArquivoPath" character varying(500) NULL;
|
||||||
|
""");
|
||||||
|
|
||||||
|
migrationBuilder.Sql("""
|
||||||
|
ALTER TABLE "Aparelhos"
|
||||||
|
ADD COLUMN IF NOT EXISTS "ReciboArquivoPath" character varying(500) NULL;
|
||||||
|
""");
|
||||||
|
|
||||||
|
// Backfill seguro para bancos que já tinham os campos antigos de URL/nome.
|
||||||
|
migrationBuilder.Sql("""
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_name = 'Aparelhos'
|
||||||
|
AND column_name = 'NotaFiscalAnexoUrl'
|
||||||
|
) THEN
|
||||||
|
UPDATE "Aparelhos"
|
||||||
|
SET "NotaFiscalArquivoPath" = COALESCE("NotaFiscalArquivoPath", "NotaFiscalAnexoUrl")
|
||||||
|
WHERE "NotaFiscalAnexoUrl" IS NOT NULL
|
||||||
|
AND "NotaFiscalAnexoUrl" <> '';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_name = 'Aparelhos'
|
||||||
|
AND column_name = 'ReciboAnexoUrl'
|
||||||
|
) THEN
|
||||||
|
UPDATE "Aparelhos"
|
||||||
|
SET "ReciboArquivoPath" = COALESCE("ReciboArquivoPath", "ReciboAnexoUrl")
|
||||||
|
WHERE "ReciboAnexoUrl" IS NOT NULL
|
||||||
|
AND "ReciboAnexoUrl" <> '';
|
||||||
|
END IF;
|
||||||
|
END
|
||||||
|
$$;
|
||||||
|
""");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.Sql("""ALTER TABLE "Aparelhos" DROP COLUMN IF EXISTS "ReciboArquivoPath";""");
|
||||||
|
migrationBuilder.Sql("""ALTER TABLE "Aparelhos" DROP COLUMN IF EXISTS "NotaFiscalArquivoPath";""");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
using line_gestao_api.Data;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace line_gestao_api.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(AppDbContext))]
|
||||||
|
[Migration("20260305193000_AddSolicitacaoLinhas")]
|
||||||
|
public class AddSolicitacaoLinhas : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.Sql("""
|
||||||
|
CREATE TABLE IF NOT EXISTS "SolicitacaoLinhas" (
|
||||||
|
"Id" uuid NOT NULL,
|
||||||
|
"TenantId" uuid NOT NULL,
|
||||||
|
"MobileLineId" uuid NULL,
|
||||||
|
"Linha" character varying(30) NULL,
|
||||||
|
"UsuarioLinha" character varying(200) NULL,
|
||||||
|
"TipoSolicitacao" character varying(60) NOT NULL,
|
||||||
|
"FranquiaLineAtual" numeric NULL,
|
||||||
|
"FranquiaLineNova" numeric NULL,
|
||||||
|
"SolicitanteUserId" uuid NULL,
|
||||||
|
"SolicitanteNome" character varying(200) NULL,
|
||||||
|
"Mensagem" character varying(1000) NOT NULL,
|
||||||
|
"Status" character varying(30) NOT NULL,
|
||||||
|
"CreatedAt" timestamp with time zone NOT NULL,
|
||||||
|
CONSTRAINT "PK_SolicitacaoLinhas" PRIMARY KEY ("Id"),
|
||||||
|
CONSTRAINT "FK_SolicitacaoLinhas_AspNetUsers_SolicitanteUserId"
|
||||||
|
FOREIGN KEY ("SolicitanteUserId") REFERENCES "AspNetUsers" ("Id")
|
||||||
|
ON DELETE SET NULL,
|
||||||
|
CONSTRAINT "FK_SolicitacaoLinhas_MobileLines_MobileLineId"
|
||||||
|
FOREIGN KEY ("MobileLineId") REFERENCES "MobileLines" ("Id")
|
||||||
|
ON DELETE SET NULL
|
||||||
|
);
|
||||||
|
""");
|
||||||
|
|
||||||
|
migrationBuilder.Sql("""CREATE INDEX IF NOT EXISTS "IX_SolicitacaoLinhas_TenantId" ON "SolicitacaoLinhas" ("TenantId");""");
|
||||||
|
migrationBuilder.Sql("""CREATE INDEX IF NOT EXISTS "IX_SolicitacaoLinhas_CreatedAt" ON "SolicitacaoLinhas" ("CreatedAt");""");
|
||||||
|
migrationBuilder.Sql("""CREATE INDEX IF NOT EXISTS "IX_SolicitacaoLinhas_TipoSolicitacao" ON "SolicitacaoLinhas" ("TipoSolicitacao");""");
|
||||||
|
migrationBuilder.Sql("""CREATE INDEX IF NOT EXISTS "IX_SolicitacaoLinhas_Status" ON "SolicitacaoLinhas" ("Status");""");
|
||||||
|
migrationBuilder.Sql("""CREATE INDEX IF NOT EXISTS "IX_SolicitacaoLinhas_MobileLineId" ON "SolicitacaoLinhas" ("MobileLineId");""");
|
||||||
|
migrationBuilder.Sql("""CREATE INDEX IF NOT EXISTS "IX_SolicitacaoLinhas_SolicitanteUserId" ON "SolicitacaoLinhas" ("SolicitanteUserId");""");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.Sql("""DROP INDEX IF EXISTS "IX_SolicitacaoLinhas_SolicitanteUserId";""");
|
||||||
|
migrationBuilder.Sql("""DROP INDEX IF EXISTS "IX_SolicitacaoLinhas_MobileLineId";""");
|
||||||
|
migrationBuilder.Sql("""DROP INDEX IF EXISTS "IX_SolicitacaoLinhas_Status";""");
|
||||||
|
migrationBuilder.Sql("""DROP INDEX IF EXISTS "IX_SolicitacaoLinhas_TipoSolicitacao";""");
|
||||||
|
migrationBuilder.Sql("""DROP INDEX IF EXISTS "IX_SolicitacaoLinhas_CreatedAt";""");
|
||||||
|
migrationBuilder.Sql("""DROP INDEX IF EXISTS "IX_SolicitacaoLinhas_TenantId";""");
|
||||||
|
migrationBuilder.Sql("""DROP TABLE IF EXISTS "SolicitacaoLinhas";""");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,160 @@
|
||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace line_gestao_api.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddMveAuditHistoryAndAparelhoFabricante : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "Fabricante",
|
||||||
|
table: "Aparelhos",
|
||||||
|
type: "character varying(120)",
|
||||||
|
maxLength: 120,
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "MveAuditRuns",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
TenantId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
FileName = table.Column<string>(type: "character varying(260)", maxLength: 260, nullable: true),
|
||||||
|
FileHashSha256 = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: true),
|
||||||
|
FileEncoding = table.Column<string>(type: "character varying(40)", maxLength: 40, nullable: true),
|
||||||
|
Status = table.Column<string>(type: "character varying(40)", maxLength: 40, nullable: false),
|
||||||
|
TotalSystemLines = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
TotalReportLines = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
TotalConciliated = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
TotalStatusDivergences = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
TotalDataDivergences = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
TotalOnlyInSystem = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
TotalOnlyInReport = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
TotalDuplicateReportLines = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
TotalDuplicateSystemLines = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
TotalInvalidRows = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
TotalUnknownStatuses = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
TotalSyncableIssues = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
AppliedIssuesCount = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
AppliedLinesCount = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
AppliedFieldsCount = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
ImportedAtUtc = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||||
|
AppliedAtUtc = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
|
||||||
|
AppliedByUserId = table.Column<Guid>(type: "uuid", nullable: true),
|
||||||
|
AppliedByUserName = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: true),
|
||||||
|
AppliedByUserEmail = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_MveAuditRuns", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "MveAuditIssues",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
TenantId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
AuditRunId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
SourceRowNumber = table.Column<int>(type: "integer", nullable: true),
|
||||||
|
NumeroLinha = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
|
||||||
|
MobileLineId = table.Column<Guid>(type: "uuid", nullable: true),
|
||||||
|
SystemItem = table.Column<int>(type: "integer", nullable: true),
|
||||||
|
IssueType = table.Column<string>(type: "character varying(60)", maxLength: 60, nullable: false),
|
||||||
|
Situation = table.Column<string>(type: "character varying(80)", maxLength: 80, nullable: false),
|
||||||
|
Severity = table.Column<string>(type: "character varying(40)", maxLength: 40, nullable: false),
|
||||||
|
Syncable = table.Column<bool>(type: "boolean", nullable: false),
|
||||||
|
Applied = table.Column<bool>(type: "boolean", nullable: false),
|
||||||
|
AppliedAtUtc = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
|
||||||
|
ActionSuggestion = table.Column<string>(type: "character varying(160)", maxLength: 160, nullable: true),
|
||||||
|
Notes = table.Column<string>(type: "character varying(500)", maxLength: 500, nullable: true),
|
||||||
|
SystemStatus = table.Column<string>(type: "character varying(120)", maxLength: 120, nullable: true),
|
||||||
|
ReportStatus = table.Column<string>(type: "character varying(120)", maxLength: 120, nullable: true),
|
||||||
|
SystemPlan = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: true),
|
||||||
|
ReportPlan = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: true),
|
||||||
|
SystemSnapshotJson = table.Column<string>(type: "jsonb", nullable: false),
|
||||||
|
ReportSnapshotJson = table.Column<string>(type: "jsonb", nullable: false),
|
||||||
|
DifferencesJson = table.Column<string>(type: "jsonb", nullable: false),
|
||||||
|
CreatedAtUtc = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_MveAuditIssues", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_MveAuditIssues_MveAuditRuns_AuditRunId",
|
||||||
|
column: x => x.AuditRunId,
|
||||||
|
principalTable: "MveAuditRuns",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Aparelhos_TenantId_Fabricante",
|
||||||
|
table: "Aparelhos",
|
||||||
|
columns: new[] { "TenantId", "Fabricante" });
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_MveAuditIssues_AuditRunId",
|
||||||
|
table: "MveAuditIssues",
|
||||||
|
column: "AuditRunId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_MveAuditIssues_IssueType",
|
||||||
|
table: "MveAuditIssues",
|
||||||
|
column: "IssueType");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_MveAuditIssues_NumeroLinha",
|
||||||
|
table: "MveAuditIssues",
|
||||||
|
column: "NumeroLinha");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_MveAuditIssues_Syncable",
|
||||||
|
table: "MveAuditIssues",
|
||||||
|
column: "Syncable");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_MveAuditIssues_TenantId",
|
||||||
|
table: "MveAuditIssues",
|
||||||
|
column: "TenantId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_MveAuditRuns_ImportedAtUtc",
|
||||||
|
table: "MveAuditRuns",
|
||||||
|
column: "ImportedAtUtc");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_MveAuditRuns_Status",
|
||||||
|
table: "MveAuditRuns",
|
||||||
|
column: "Status");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_MveAuditRuns_TenantId",
|
||||||
|
table: "MveAuditRuns",
|
||||||
|
column: "TenantId");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "MveAuditIssues");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "MveAuditRuns");
|
||||||
|
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "IX_Aparelhos_TenantId_Fabricante",
|
||||||
|
table: "Aparelhos");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "Fabricante",
|
||||||
|
table: "Aparelhos");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -330,6 +330,58 @@ namespace line_gestao_api.Migrations
|
||||||
b.ToTable("AuditLogs");
|
b.ToTable("AuditLogs");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("line_gestao_api.Models.Aparelho", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("Cor")
|
||||||
|
.HasMaxLength(80)
|
||||||
|
.HasColumnType("character varying(80)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("Fabricante")
|
||||||
|
.HasMaxLength(120)
|
||||||
|
.HasColumnType("character varying(120)");
|
||||||
|
|
||||||
|
b.Property<string>("Imei")
|
||||||
|
.HasMaxLength(80)
|
||||||
|
.HasColumnType("character varying(80)");
|
||||||
|
|
||||||
|
b.Property<string>("Nome")
|
||||||
|
.HasMaxLength(160)
|
||||||
|
.HasColumnType("character varying(160)");
|
||||||
|
|
||||||
|
b.Property<string>("NotaFiscalArquivoPath")
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("character varying(500)");
|
||||||
|
|
||||||
|
b.Property<string>("ReciboArquivoPath")
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("character varying(500)");
|
||||||
|
|
||||||
|
b.Property<Guid>("TenantId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Imei");
|
||||||
|
|
||||||
|
b.HasIndex("TenantId");
|
||||||
|
|
||||||
|
b.HasIndex("TenantId", "Fabricante");
|
||||||
|
|
||||||
|
b.HasIndex("TenantId", "Nome", "Cor");
|
||||||
|
|
||||||
|
b.ToTable("Aparelhos");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("line_gestao_api.Models.BillingClient", b =>
|
modelBuilder.Entity("line_gestao_api.Models.BillingClient", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
|
|
@ -618,16 +670,226 @@ namespace line_gestao_api.Migrations
|
||||||
b.ToTable("ImportAuditRuns");
|
b.ToTable("ImportAuditRuns");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("line_gestao_api.Models.MveAuditIssue", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<Guid>("AuditRunId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("ActionSuggestion")
|
||||||
|
.HasMaxLength(160)
|
||||||
|
.HasColumnType("character varying(160)");
|
||||||
|
|
||||||
|
b.Property<bool>("Applied")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("AppliedAtUtc")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAtUtc")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("DifferencesJson")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("jsonb");
|
||||||
|
|
||||||
|
b.Property<string>("IssueType")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(60)
|
||||||
|
.HasColumnType("character varying(60)");
|
||||||
|
|
||||||
|
b.Property<Guid?>("MobileLineId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("Notes")
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("character varying(500)");
|
||||||
|
|
||||||
|
b.Property<string>("NumeroLinha")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("character varying(64)");
|
||||||
|
|
||||||
|
b.Property<string>("ReportPlan")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("character varying(200)");
|
||||||
|
|
||||||
|
b.Property<string>("ReportSnapshotJson")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("jsonb");
|
||||||
|
|
||||||
|
b.Property<string>("ReportStatus")
|
||||||
|
.HasMaxLength(120)
|
||||||
|
.HasColumnType("character varying(120)");
|
||||||
|
|
||||||
|
b.Property<string>("Severity")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(40)
|
||||||
|
.HasColumnType("character varying(40)");
|
||||||
|
|
||||||
|
b.Property<int?>("SourceRowNumber")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<string>("Situation")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(80)
|
||||||
|
.HasColumnType("character varying(80)");
|
||||||
|
|
||||||
|
b.Property<bool>("Syncable")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<string>("SystemPlan")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("character varying(200)");
|
||||||
|
|
||||||
|
b.Property<int?>("SystemItem")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<string>("SystemSnapshotJson")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("jsonb");
|
||||||
|
|
||||||
|
b.Property<string>("SystemStatus")
|
||||||
|
.HasMaxLength(120)
|
||||||
|
.HasColumnType("character varying(120)");
|
||||||
|
|
||||||
|
b.Property<Guid>("TenantId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("AuditRunId");
|
||||||
|
|
||||||
|
b.HasIndex("IssueType");
|
||||||
|
|
||||||
|
b.HasIndex("NumeroLinha");
|
||||||
|
|
||||||
|
b.HasIndex("Syncable");
|
||||||
|
|
||||||
|
b.HasIndex("TenantId");
|
||||||
|
|
||||||
|
b.ToTable("MveAuditIssues");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("line_gestao_api.Models.MveAuditRun", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("AppliedAtUtc")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("AppliedByUserEmail")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("character varying(200)");
|
||||||
|
|
||||||
|
b.Property<Guid?>("AppliedByUserId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("AppliedByUserName")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("character varying(200)");
|
||||||
|
|
||||||
|
b.Property<int>("AppliedFieldsCount")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("AppliedIssuesCount")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("AppliedLinesCount")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<string>("FileEncoding")
|
||||||
|
.HasMaxLength(40)
|
||||||
|
.HasColumnType("character varying(40)");
|
||||||
|
|
||||||
|
b.Property<string>("FileHashSha256")
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("character varying(64)");
|
||||||
|
|
||||||
|
b.Property<string>("FileName")
|
||||||
|
.HasMaxLength(260)
|
||||||
|
.HasColumnType("character varying(260)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("ImportedAtUtc")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("Status")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(40)
|
||||||
|
.HasColumnType("character varying(40)");
|
||||||
|
|
||||||
|
b.Property<Guid>("TenantId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<int>("TotalConciliated")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("TotalDataDivergences")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("TotalDuplicateReportLines")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("TotalDuplicateSystemLines")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("TotalInvalidRows")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("TotalOnlyInReport")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("TotalOnlyInSystem")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("TotalReportLines")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("TotalStatusDivergences")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("TotalSyncableIssues")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("TotalSystemLines")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("TotalUnknownStatuses")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("ImportedAtUtc");
|
||||||
|
|
||||||
|
b.HasIndex("Status");
|
||||||
|
|
||||||
|
b.HasIndex("TenantId");
|
||||||
|
|
||||||
|
b.ToTable("MveAuditRuns");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("line_gestao_api.Models.MobileLine", b =>
|
modelBuilder.Entity("line_gestao_api.Models.MobileLine", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
.HasColumnType("uuid");
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<Guid?>("AparelhoId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
b.Property<string>("Cedente")
|
b.Property<string>("Cedente")
|
||||||
.HasMaxLength(150)
|
.HasMaxLength(150)
|
||||||
.HasColumnType("character varying(150)");
|
.HasColumnType("character varying(150)");
|
||||||
|
|
||||||
|
b.Property<string>("CentroDeCustos")
|
||||||
|
.HasMaxLength(180)
|
||||||
|
.HasColumnType("character varying(180)");
|
||||||
|
|
||||||
b.Property<string>("Chip")
|
b.Property<string>("Chip")
|
||||||
.HasMaxLength(40)
|
.HasMaxLength(40)
|
||||||
.HasColumnType("character varying(40)");
|
.HasColumnType("character varying(40)");
|
||||||
|
|
@ -691,6 +953,9 @@ namespace line_gestao_api.Migrations
|
||||||
b.Property<decimal?>("Skeelo")
|
b.Property<decimal?>("Skeelo")
|
||||||
.HasColumnType("numeric");
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<Guid?>("SetorId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
b.Property<string>("Skil")
|
b.Property<string>("Skil")
|
||||||
.HasMaxLength(80)
|
.HasMaxLength(80)
|
||||||
.HasColumnType("character varying(80)");
|
.HasColumnType("character varying(80)");
|
||||||
|
|
@ -744,10 +1009,16 @@ namespace line_gestao_api.Migrations
|
||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("AparelhoId");
|
||||||
|
|
||||||
b.HasIndex("Chip");
|
b.HasIndex("Chip");
|
||||||
|
|
||||||
|
b.HasIndex("CentroDeCustos");
|
||||||
|
|
||||||
b.HasIndex("Cliente");
|
b.HasIndex("Cliente");
|
||||||
|
|
||||||
|
b.HasIndex("SetorId");
|
||||||
|
|
||||||
b.HasIndex("Skil");
|
b.HasIndex("Skil");
|
||||||
|
|
||||||
b.HasIndex("Status");
|
b.HasIndex("Status");
|
||||||
|
|
@ -888,6 +1159,74 @@ namespace line_gestao_api.Migrations
|
||||||
b.ToTable("Notifications");
|
b.ToTable("Notifications");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("line_gestao_api.Models.SolicitacaoLinha", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<decimal?>("FranquiaLineAtual")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("FranquiaLineNova")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<string>("Linha")
|
||||||
|
.HasMaxLength(30)
|
||||||
|
.HasColumnType("character varying(30)");
|
||||||
|
|
||||||
|
b.Property<string>("Mensagem")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(1000)
|
||||||
|
.HasColumnType("character varying(1000)");
|
||||||
|
|
||||||
|
b.Property<Guid?>("MobileLineId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("SolicitanteNome")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("character varying(200)");
|
||||||
|
|
||||||
|
b.Property<Guid?>("SolicitanteUserId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("Status")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(30)
|
||||||
|
.HasColumnType("character varying(30)");
|
||||||
|
|
||||||
|
b.Property<Guid>("TenantId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("TipoSolicitacao")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(60)
|
||||||
|
.HasColumnType("character varying(60)");
|
||||||
|
|
||||||
|
b.Property<string>("UsuarioLinha")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("character varying(200)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("CreatedAt");
|
||||||
|
|
||||||
|
b.HasIndex("MobileLineId");
|
||||||
|
|
||||||
|
b.HasIndex("SolicitanteUserId");
|
||||||
|
|
||||||
|
b.HasIndex("Status");
|
||||||
|
|
||||||
|
b.HasIndex("TenantId");
|
||||||
|
|
||||||
|
b.HasIndex("TipoSolicitacao");
|
||||||
|
|
||||||
|
b.ToTable("SolicitacaoLinhas");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("line_gestao_api.Models.ParcelamentoLine", b =>
|
modelBuilder.Entity("line_gestao_api.Models.ParcelamentoLine", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
|
|
@ -1370,6 +1709,36 @@ namespace line_gestao_api.Migrations
|
||||||
b.ToTable("ResumoVivoLineTotals");
|
b.ToTable("ResumoVivoLineTotals");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("line_gestao_api.Models.Setor", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("Nome")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(160)
|
||||||
|
.HasColumnType("character varying(160)");
|
||||||
|
|
||||||
|
b.Property<Guid>("TenantId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("TenantId");
|
||||||
|
|
||||||
|
b.HasIndex("TenantId", "Nome")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("Setores");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("line_gestao_api.Models.Tenant", b =>
|
modelBuilder.Entity("line_gestao_api.Models.Tenant", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
|
|
@ -1665,6 +2034,23 @@ namespace line_gestao_api.Migrations
|
||||||
.IsRequired();
|
.IsRequired();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("line_gestao_api.Models.MobileLine", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("line_gestao_api.Models.Aparelho", "Aparelho")
|
||||||
|
.WithMany("MobileLines")
|
||||||
|
.HasForeignKey("AparelhoId")
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
|
||||||
|
b.HasOne("line_gestao_api.Models.Setor", "Setor")
|
||||||
|
.WithMany("MobileLines")
|
||||||
|
.HasForeignKey("SetorId")
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
|
||||||
|
b.Navigation("Aparelho");
|
||||||
|
|
||||||
|
b.Navigation("Setor");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("line_gestao_api.Models.MuregLine", b =>
|
modelBuilder.Entity("line_gestao_api.Models.MuregLine", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("line_gestao_api.Models.MobileLine", "MobileLine")
|
b.HasOne("line_gestao_api.Models.MobileLine", "MobileLine")
|
||||||
|
|
@ -1687,6 +2073,17 @@ namespace line_gestao_api.Migrations
|
||||||
b.Navigation("AuditRun");
|
b.Navigation("AuditRun");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("line_gestao_api.Models.MveAuditIssue", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("line_gestao_api.Models.MveAuditRun", "AuditRun")
|
||||||
|
.WithMany("Issues")
|
||||||
|
.HasForeignKey("AuditRunId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("AuditRun");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("line_gestao_api.Models.Notification", b =>
|
modelBuilder.Entity("line_gestao_api.Models.Notification", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("line_gestao_api.Models.ApplicationUser", "User")
|
b.HasOne("line_gestao_api.Models.ApplicationUser", "User")
|
||||||
|
|
@ -1704,6 +2101,23 @@ namespace line_gestao_api.Migrations
|
||||||
b.Navigation("VigenciaLine");
|
b.Navigation("VigenciaLine");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("line_gestao_api.Models.SolicitacaoLinha", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("line_gestao_api.Models.MobileLine", "MobileLine")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("MobileLineId")
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
|
||||||
|
b.HasOne("line_gestao_api.Models.ApplicationUser", "SolicitanteUser")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("SolicitanteUserId")
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
|
||||||
|
b.Navigation("MobileLine");
|
||||||
|
|
||||||
|
b.Navigation("SolicitanteUser");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("line_gestao_api.Models.ParcelamentoMonthValue", b =>
|
modelBuilder.Entity("line_gestao_api.Models.ParcelamentoMonthValue", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("line_gestao_api.Models.ParcelamentoLine", "ParcelamentoLine")
|
b.HasOne("line_gestao_api.Models.ParcelamentoLine", "ParcelamentoLine")
|
||||||
|
|
@ -1715,6 +2129,11 @@ namespace line_gestao_api.Migrations
|
||||||
b.Navigation("ParcelamentoLine");
|
b.Navigation("ParcelamentoLine");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("line_gestao_api.Models.Aparelho", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("MobileLines");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("line_gestao_api.Models.MobileLine", b =>
|
modelBuilder.Entity("line_gestao_api.Models.MobileLine", b =>
|
||||||
{
|
{
|
||||||
b.Navigation("Muregs");
|
b.Navigation("Muregs");
|
||||||
|
|
@ -1725,10 +2144,20 @@ namespace line_gestao_api.Migrations
|
||||||
b.Navigation("Issues");
|
b.Navigation("Issues");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("line_gestao_api.Models.MveAuditRun", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Issues");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("line_gestao_api.Models.ParcelamentoLine", b =>
|
modelBuilder.Entity("line_gestao_api.Models.ParcelamentoLine", b =>
|
||||||
{
|
{
|
||||||
b.Navigation("MonthValues");
|
b.Navigation("MonthValues");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("line_gestao_api.Models.Setor", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("MobileLines");
|
||||||
|
});
|
||||||
#pragma warning restore 612, 618
|
#pragma warning restore 612, 618
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace line_gestao_api.Models;
|
||||||
|
|
||||||
|
public class Aparelho : ITenantEntity
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; } = Guid.NewGuid();
|
||||||
|
|
||||||
|
public Guid TenantId { get; set; }
|
||||||
|
|
||||||
|
[MaxLength(160)]
|
||||||
|
public string? Nome { get; set; }
|
||||||
|
|
||||||
|
[MaxLength(120)]
|
||||||
|
public string? Fabricante { get; set; }
|
||||||
|
|
||||||
|
[MaxLength(80)]
|
||||||
|
public string? Cor { get; set; }
|
||||||
|
|
||||||
|
[MaxLength(80)]
|
||||||
|
public string? Imei { get; set; }
|
||||||
|
|
||||||
|
[MaxLength(500)]
|
||||||
|
public string? NotaFiscalArquivoPath { get; set; }
|
||||||
|
|
||||||
|
[MaxLength(500)]
|
||||||
|
public string? ReciboArquivoPath { get; set; }
|
||||||
|
|
||||||
|
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||||
|
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
|
||||||
|
|
||||||
|
public ICollection<MobileLine> MobileLines { get; set; } = new List<MobileLine>();
|
||||||
|
}
|
||||||
|
|
@ -20,6 +20,15 @@ namespace line_gestao_api.Models
|
||||||
public string? Cliente { get; set; }
|
public string? Cliente { get; set; }
|
||||||
[MaxLength(200)]
|
[MaxLength(200)]
|
||||||
public string? Usuario { get; set; }
|
public string? Usuario { get; set; }
|
||||||
|
[MaxLength(180)]
|
||||||
|
public string? CentroDeCustos { get; set; }
|
||||||
|
|
||||||
|
public Guid? SetorId { get; set; }
|
||||||
|
public Setor? Setor { get; set; }
|
||||||
|
|
||||||
|
public Guid? AparelhoId { get; set; }
|
||||||
|
public Aparelho? Aparelho { get; set; }
|
||||||
|
|
||||||
[MaxLength(200)]
|
[MaxLength(200)]
|
||||||
public string? PlanoContrato { get; set; }
|
public string? PlanoContrato { get; set; }
|
||||||
|
|
||||||
|
|
@ -66,7 +75,6 @@ namespace line_gestao_api.Models
|
||||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||||
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
|
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
|
||||||
|
|
||||||
// ✅ Navegação (1 MobileLine -> N Muregs)
|
|
||||||
public ICollection<MuregLine> Muregs { get; set; } = new List<MuregLine>();
|
public ICollection<MuregLine> Muregs { get; set; } = new List<MuregLine>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
namespace line_gestao_api.Models;
|
||||||
|
|
||||||
|
public class MveAuditIssue : ITenantEntity
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; } = Guid.NewGuid();
|
||||||
|
|
||||||
|
public Guid TenantId { get; set; }
|
||||||
|
|
||||||
|
public Guid AuditRunId { get; set; }
|
||||||
|
|
||||||
|
public MveAuditRun? AuditRun { get; set; }
|
||||||
|
|
||||||
|
public int? SourceRowNumber { get; set; }
|
||||||
|
|
||||||
|
public string NumeroLinha { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public Guid? MobileLineId { get; set; }
|
||||||
|
|
||||||
|
public int? SystemItem { get; set; }
|
||||||
|
|
||||||
|
public string IssueType { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string Situation { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string Severity { get; set; } = "INFO";
|
||||||
|
|
||||||
|
public bool Syncable { get; set; }
|
||||||
|
|
||||||
|
public bool Applied { get; set; }
|
||||||
|
|
||||||
|
public DateTime? AppliedAtUtc { get; set; }
|
||||||
|
|
||||||
|
public string? ActionSuggestion { get; set; }
|
||||||
|
|
||||||
|
public string? Notes { get; set; }
|
||||||
|
|
||||||
|
public string? SystemStatus { get; set; }
|
||||||
|
|
||||||
|
public string? ReportStatus { get; set; }
|
||||||
|
|
||||||
|
public string? SystemPlan { get; set; }
|
||||||
|
|
||||||
|
public string? ReportPlan { get; set; }
|
||||||
|
|
||||||
|
public string SystemSnapshotJson { get; set; } = "{}";
|
||||||
|
|
||||||
|
public string ReportSnapshotJson { get; set; } = "{}";
|
||||||
|
|
||||||
|
public string DifferencesJson { get; set; } = "[]";
|
||||||
|
|
||||||
|
public DateTime CreatedAtUtc { get; set; } = DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
namespace line_gestao_api.Models;
|
||||||
|
|
||||||
|
public class MveAuditRun : ITenantEntity
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; } = Guid.NewGuid();
|
||||||
|
|
||||||
|
public Guid TenantId { get; set; }
|
||||||
|
|
||||||
|
public string? FileName { get; set; }
|
||||||
|
|
||||||
|
public string? FileHashSha256 { get; set; }
|
||||||
|
|
||||||
|
public string? FileEncoding { get; set; }
|
||||||
|
|
||||||
|
public string Status { get; set; } = "READY";
|
||||||
|
|
||||||
|
public int TotalSystemLines { get; set; }
|
||||||
|
|
||||||
|
public int TotalReportLines { get; set; }
|
||||||
|
|
||||||
|
public int TotalConciliated { get; set; }
|
||||||
|
|
||||||
|
public int TotalStatusDivergences { get; set; }
|
||||||
|
|
||||||
|
public int TotalDataDivergences { get; set; }
|
||||||
|
|
||||||
|
public int TotalOnlyInSystem { get; set; }
|
||||||
|
|
||||||
|
public int TotalOnlyInReport { get; set; }
|
||||||
|
|
||||||
|
public int TotalDuplicateReportLines { get; set; }
|
||||||
|
|
||||||
|
public int TotalDuplicateSystemLines { get; set; }
|
||||||
|
|
||||||
|
public int TotalInvalidRows { get; set; }
|
||||||
|
|
||||||
|
public int TotalUnknownStatuses { get; set; }
|
||||||
|
|
||||||
|
public int TotalSyncableIssues { get; set; }
|
||||||
|
|
||||||
|
public int AppliedIssuesCount { get; set; }
|
||||||
|
|
||||||
|
public int AppliedLinesCount { get; set; }
|
||||||
|
|
||||||
|
public int AppliedFieldsCount { get; set; }
|
||||||
|
|
||||||
|
public DateTime ImportedAtUtc { get; set; } = DateTime.UtcNow;
|
||||||
|
|
||||||
|
public DateTime? AppliedAtUtc { get; set; }
|
||||||
|
|
||||||
|
public Guid? AppliedByUserId { get; set; }
|
||||||
|
|
||||||
|
public string? AppliedByUserName { get; set; }
|
||||||
|
|
||||||
|
public string? AppliedByUserEmail { get; set; }
|
||||||
|
|
||||||
|
public ICollection<MveAuditIssue> Issues { get; set; } = new List<MveAuditIssue>();
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace line_gestao_api.Models;
|
||||||
|
|
||||||
|
public class Setor : ITenantEntity
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; } = Guid.NewGuid();
|
||||||
|
|
||||||
|
public Guid TenantId { get; set; }
|
||||||
|
|
||||||
|
[MaxLength(160)]
|
||||||
|
public string Nome { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||||
|
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
|
||||||
|
|
||||||
|
public ICollection<MobileLine> MobileLines { get; set; } = new List<MobileLine>();
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace line_gestao_api.Models;
|
||||||
|
|
||||||
|
public class SolicitacaoLinha : ITenantEntity
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; } = Guid.NewGuid();
|
||||||
|
|
||||||
|
public Guid TenantId { get; set; }
|
||||||
|
|
||||||
|
public Guid? MobileLineId { get; set; }
|
||||||
|
public MobileLine? MobileLine { get; set; }
|
||||||
|
|
||||||
|
[MaxLength(30)]
|
||||||
|
public string? Linha { get; set; }
|
||||||
|
|
||||||
|
[MaxLength(200)]
|
||||||
|
public string? UsuarioLinha { get; set; }
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
[MaxLength(60)]
|
||||||
|
public string TipoSolicitacao { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public decimal? FranquiaLineAtual { get; set; }
|
||||||
|
public decimal? FranquiaLineNova { get; set; }
|
||||||
|
|
||||||
|
public Guid? SolicitanteUserId { get; set; }
|
||||||
|
public ApplicationUser? SolicitanteUser { get; set; }
|
||||||
|
|
||||||
|
[MaxLength(200)]
|
||||||
|
public string? SolicitanteNome { get; set; }
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
[MaxLength(1000)]
|
||||||
|
public string Mensagem { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
[MaxLength(30)]
|
||||||
|
public string Status { get; set; } = "PENDENTE";
|
||||||
|
|
||||||
|
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||||
|
}
|
||||||
11
Program.cs
11
Program.cs
|
|
@ -16,6 +16,7 @@ using Microsoft.IdentityModel.Tokens;
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
builder.Configuration.AddJsonFile("appsettings.Local.json", optional: true, reloadOnChange: true);
|
builder.Configuration.AddJsonFile("appsettings.Local.json", optional: true, reloadOnChange: true);
|
||||||
|
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
|
||||||
|
|
||||||
var dataProtectionKeyPath = builder.Environment.IsProduction()
|
var dataProtectionKeyPath = builder.Environment.IsProduction()
|
||||||
? "/var/www/html/line-gestao-api/publish/.aspnet-keys"
|
? "/var/www/html/line-gestao-api/publish/.aspnet-keys"
|
||||||
|
|
@ -96,8 +97,13 @@ 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<DashboardKpiSnapshotService>();
|
||||||
builder.Services.AddScoped<GeralSpreadsheetTemplateService>();
|
builder.Services.AddScoped<GeralSpreadsheetTemplateService>();
|
||||||
builder.Services.AddScoped<SpreadsheetImportAuditService>();
|
builder.Services.AddScoped<SpreadsheetImportAuditService>();
|
||||||
|
builder.Services.AddScoped<MveCsvParserService>();
|
||||||
|
builder.Services.AddScoped<MveReconciliationService>();
|
||||||
|
builder.Services.AddScoped<MveAuditService>();
|
||||||
|
builder.Services.AddScoped<MveAuditSchemaBootstrapper>();
|
||||||
|
|
||||||
builder.Services.AddIdentityCore<ApplicationUser>(options =>
|
builder.Services.AddIdentityCore<ApplicationUser>(options =>
|
||||||
{
|
{
|
||||||
|
|
@ -197,6 +203,11 @@ app.UseMiddleware<TenantMiddleware>();
|
||||||
app.UseAuthorization();
|
app.UseAuthorization();
|
||||||
|
|
||||||
await SeedData.EnsureSeedDataAsync(app.Services);
|
await SeedData.EnsureSeedDataAsync(app.Services);
|
||||||
|
using (var scope = app.Services.CreateScope())
|
||||||
|
{
|
||||||
|
var schemaBootstrapper = scope.ServiceProvider.GetRequiredService<MveAuditSchemaBootstrapper>();
|
||||||
|
await schemaBootstrapper.EnsureSchemaAsync();
|
||||||
|
}
|
||||||
|
|
||||||
app.MapControllers();
|
app.MapControllers();
|
||||||
app.MapGet("/health", () => Results.Ok(new { status = "ok" }));
|
app.MapGet("/health", () => Results.Ok(new { status = "ok" }));
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,8 @@ public static class AppRoles
|
||||||
{
|
{
|
||||||
public const string SysAdmin = "sysadmin";
|
public const string SysAdmin = "sysadmin";
|
||||||
public const string Gestor = "gestor";
|
public const string Gestor = "gestor";
|
||||||
|
public const string Financeiro = "financeiro";
|
||||||
public const string Cliente = "cliente";
|
public const string Cliente = "cliente";
|
||||||
|
|
||||||
public static readonly string[] All = [SysAdmin, Gestor, Cliente];
|
public static readonly string[] All = [SysAdmin, Gestor, Financeiro, Cliente];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,380 @@
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Security.Claims;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using line_gestao_api.Data;
|
||||||
|
using line_gestao_api.Models;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace line_gestao_api.Services;
|
||||||
|
|
||||||
|
public sealed class DashboardKpiSnapshotService
|
||||||
|
{
|
||||||
|
private const string SnapshotEntityName = "DashboardKpiSnapshot";
|
||||||
|
|
||||||
|
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web);
|
||||||
|
|
||||||
|
private readonly AppDbContext _db;
|
||||||
|
private readonly ITenantProvider _tenantProvider;
|
||||||
|
|
||||||
|
public DashboardKpiSnapshotService(AppDbContext db, ITenantProvider tenantProvider)
|
||||||
|
{
|
||||||
|
_db = db;
|
||||||
|
_tenantProvider = tenantProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task CaptureAfterSpreadsheetImportAsync(
|
||||||
|
ClaimsPrincipal user,
|
||||||
|
HttpContext httpContext,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var tenantId = _tenantProvider.TenantId;
|
||||||
|
if (!tenantId.HasValue)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var userId = TryParseUserId(user);
|
||||||
|
var userName = user.FindFirst("name")?.Value
|
||||||
|
?? user.FindFirst(ClaimTypes.Name)?.Value
|
||||||
|
?? user.Identity?.Name
|
||||||
|
?? "USUARIO";
|
||||||
|
var userEmail = user.FindFirst(ClaimTypes.Email)?.Value
|
||||||
|
?? user.FindFirst("email")?.Value;
|
||||||
|
|
||||||
|
foreach (var scope in EnumerateScopes())
|
||||||
|
{
|
||||||
|
var snapshot = await BuildSnapshotAsync(scope.Operadora, cancellationToken);
|
||||||
|
|
||||||
|
_db.AuditLogs.Add(new AuditLog
|
||||||
|
{
|
||||||
|
TenantId = tenantId.Value,
|
||||||
|
ActorUserId = userId,
|
||||||
|
ActorTenantId = tenantId.Value,
|
||||||
|
TargetTenantId = tenantId.Value,
|
||||||
|
OccurredAtUtc = DateTime.UtcNow,
|
||||||
|
UserId = userId,
|
||||||
|
UserName = userName,
|
||||||
|
UserEmail = userEmail,
|
||||||
|
Action = "SNAPSHOT",
|
||||||
|
Page = AuditLogBuilder.SpreadsheetImportPageName,
|
||||||
|
EntityName = SnapshotEntityName,
|
||||||
|
EntityId = snapshot.Scope,
|
||||||
|
EntityLabel = $"Snapshot Dashboard {snapshot.Scope}",
|
||||||
|
ChangesJson = "[]",
|
||||||
|
MetadataJson = JsonSerializer.Serialize(snapshot, JsonOptions),
|
||||||
|
RequestPath = httpContext.Request.Path.Value,
|
||||||
|
RequestMethod = httpContext.Request.Method,
|
||||||
|
IpAddress = httpContext.Connection.RemoteIpAddress?.ToString()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await _db.SaveChangesAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Dictionary<string, string>> GetTrendMapAsync(
|
||||||
|
string? operadora = null,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var scope = NormalizeScope(operadora);
|
||||||
|
var logs = await _db.AuditLogs
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(x =>
|
||||||
|
x.Page == AuditLogBuilder.SpreadsheetImportPageName &&
|
||||||
|
x.EntityName == SnapshotEntityName &&
|
||||||
|
x.EntityId == scope)
|
||||||
|
.OrderByDescending(x => x.OccurredAtUtc)
|
||||||
|
.Take(8)
|
||||||
|
.Select(x => new { x.OccurredAtUtc, x.MetadataJson })
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
if (logs.Count < 2)
|
||||||
|
{
|
||||||
|
return new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
var latest = DeserializeSnapshot(logs[0].MetadataJson);
|
||||||
|
if (latest == null)
|
||||||
|
{
|
||||||
|
return new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
var baselineThreshold = logs[0].OccurredAtUtc.AddHours(-24);
|
||||||
|
var previous = logs
|
||||||
|
.Skip(1)
|
||||||
|
.Select(x => DeserializeSnapshot(x.MetadataJson))
|
||||||
|
.FirstOrDefault(x => x != null && x.CapturedAtUtc >= baselineThreshold);
|
||||||
|
|
||||||
|
if (previous == null)
|
||||||
|
{
|
||||||
|
return new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
var keys = latest.Metrics.Keys
|
||||||
|
.Concat(previous.Metrics.Keys)
|
||||||
|
.Distinct(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
var result = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
foreach (var key in keys)
|
||||||
|
{
|
||||||
|
latest.Metrics.TryGetValue(key, out var currentValue);
|
||||||
|
previous.Metrics.TryGetValue(key, out var previousValue);
|
||||||
|
result[key] = currentValue > previousValue
|
||||||
|
? "up"
|
||||||
|
: currentValue < previousValue
|
||||||
|
? "down"
|
||||||
|
: "stable";
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<DashboardKpiSnapshotPayload> BuildSnapshotAsync(
|
||||||
|
string? operadora,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var todayUtcStart = DateTime.SpecifyKind(DateTime.UtcNow.Date, DateTimeKind.Utc);
|
||||||
|
var tomorrowUtcStart = todayUtcStart.AddDays(1);
|
||||||
|
var last30UtcStart = todayUtcStart.AddDays(-30);
|
||||||
|
var limit30ExclusiveUtcStart = todayUtcStart.AddDays(31);
|
||||||
|
|
||||||
|
var qLines = OperadoraContaResolver.ApplyOperadoraFilter(_db.MobileLines.AsNoTracking(), operadora);
|
||||||
|
var qReserva = ApplyReservaContextFilter(qLines);
|
||||||
|
var qOperacionais = ExcludeReservaContext(qLines);
|
||||||
|
var qOperacionaisWithClient = qOperacionais.Where(x => x.Cliente != null && x.Cliente != "");
|
||||||
|
var qNonReserva = ExcludeReservaContext(qLines);
|
||||||
|
|
||||||
|
var totalLinhas = await qLines.CountAsync(cancellationToken);
|
||||||
|
var linhasAtivas = await qOperacionais.CountAsync(x =>
|
||||||
|
EF.Functions.ILike((x.Status ?? "").Trim(), "%ativo%"), cancellationToken);
|
||||||
|
var linhasBloqueadas = await qOperacionaisWithClient.CountAsync(x =>
|
||||||
|
EF.Functions.ILike((x.Status ?? "").Trim(), "%bloque%") ||
|
||||||
|
EF.Functions.ILike((x.Status ?? "").Trim(), "%perda%") ||
|
||||||
|
EF.Functions.ILike((x.Status ?? "").Trim(), "%roubo%"), cancellationToken);
|
||||||
|
var linhasReserva = await qReserva.CountAsync(cancellationToken);
|
||||||
|
|
||||||
|
var franquiaVivoTotal = await qLines.SumAsync(x => x.FranquiaVivo ?? 0m, cancellationToken);
|
||||||
|
var franquiaLineTotal = await qLines.SumAsync(x => x.FranquiaLine ?? 0m, cancellationToken);
|
||||||
|
|
||||||
|
var vigVencidos = await _db.VigenciaLines.AsNoTracking()
|
||||||
|
.CountAsync(x =>
|
||||||
|
x.DtTerminoFidelizacao != null &&
|
||||||
|
x.DtTerminoFidelizacao.Value < todayUtcStart, cancellationToken);
|
||||||
|
|
||||||
|
var vigencia30 = await _db.VigenciaLines.AsNoTracking()
|
||||||
|
.CountAsync(x =>
|
||||||
|
x.DtTerminoFidelizacao != null &&
|
||||||
|
x.DtTerminoFidelizacao.Value >= todayUtcStart &&
|
||||||
|
x.DtTerminoFidelizacao.Value < limit30ExclusiveUtcStart, cancellationToken);
|
||||||
|
|
||||||
|
var mureg30 = await _db.MuregLines.AsNoTracking()
|
||||||
|
.CountAsync(x =>
|
||||||
|
x.DataDaMureg != null &&
|
||||||
|
x.DataDaMureg.Value >= last30UtcStart &&
|
||||||
|
x.DataDaMureg.Value < tomorrowUtcStart, cancellationToken);
|
||||||
|
|
||||||
|
var troca30 = await _db.TrocaNumeroLines.AsNoTracking()
|
||||||
|
.CountAsync(x =>
|
||||||
|
x.DataTroca != null &&
|
||||||
|
x.DataTroca.Value >= last30UtcStart &&
|
||||||
|
x.DataTroca.Value < tomorrowUtcStart, cancellationToken);
|
||||||
|
|
||||||
|
var qLineItems = qLines
|
||||||
|
.Where(x => x.Item > 0)
|
||||||
|
.Select(x => x.Item);
|
||||||
|
var qLineNumbers = qLines
|
||||||
|
.Where(x => x.Linha != null && x.Linha != "")
|
||||||
|
.Select(x => x.Linha!);
|
||||||
|
|
||||||
|
var qUserData = _db.UserDatas.AsNoTracking()
|
||||||
|
.Where(x =>
|
||||||
|
(x.Item > 0 && qLineItems.Contains(x.Item)) ||
|
||||||
|
(x.Linha != null && x.Linha != "" && qLineNumbers.Contains(x.Linha)));
|
||||||
|
|
||||||
|
var cadastrosTotal = await qUserData.CountAsync(cancellationToken);
|
||||||
|
|
||||||
|
var travelCom = await qLines.CountAsync(x => (x.VivoTravelMundo ?? 0m) > 0m, cancellationToken);
|
||||||
|
var adicionalPago = await qLines.CountAsync(x =>
|
||||||
|
(x.GestaoVozDados ?? 0m) > 0m ||
|
||||||
|
(x.Skeelo ?? 0m) > 0m ||
|
||||||
|
(x.VivoNewsPlus ?? 0m) > 0m ||
|
||||||
|
(x.VivoTravelMundo ?? 0m) > 0m ||
|
||||||
|
(x.VivoSync ?? 0m) > 0m ||
|
||||||
|
(x.VivoGestaoDispositivo ?? 0m) > 0m, cancellationToken);
|
||||||
|
|
||||||
|
var planosContratados = await qNonReserva
|
||||||
|
.Where(x => x.PlanoContrato != null && x.PlanoContrato != "")
|
||||||
|
.Select(x => (x.PlanoContrato ?? "").Trim())
|
||||||
|
.Distinct()
|
||||||
|
.CountAsync(cancellationToken);
|
||||||
|
|
||||||
|
var usuariosRaw = await qNonReserva
|
||||||
|
.Where(x => x.Usuario != null && x.Usuario != "")
|
||||||
|
.Select(x => new
|
||||||
|
{
|
||||||
|
Usuario = (x.Usuario ?? "").Trim(),
|
||||||
|
Status = (x.Status ?? "").Trim()
|
||||||
|
})
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
var usuariosComLinha = usuariosRaw
|
||||||
|
.Where(x => ShouldIncludeUsuario(x.Usuario, x.Status))
|
||||||
|
.Select(x => x.Usuario)
|
||||||
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
|
.Count();
|
||||||
|
|
||||||
|
return new DashboardKpiSnapshotPayload
|
||||||
|
{
|
||||||
|
Scope = NormalizeScope(operadora),
|
||||||
|
CapturedAtUtc = DateTime.UtcNow,
|
||||||
|
Metrics = new Dictionary<string, decimal>(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
["linhas_total"] = totalLinhas,
|
||||||
|
["linhas_ativas"] = linhasAtivas,
|
||||||
|
["linhas_bloqueadas"] = linhasBloqueadas,
|
||||||
|
["linhas_reserva"] = linhasReserva,
|
||||||
|
["franquia_vivo_total"] = franquiaVivoTotal,
|
||||||
|
["franquia_line_total"] = franquiaLineTotal,
|
||||||
|
["vig_vencidos"] = vigVencidos,
|
||||||
|
["vig_30"] = vigencia30,
|
||||||
|
["mureg_30"] = mureg30,
|
||||||
|
["troca_30"] = troca30,
|
||||||
|
["cadastros_total"] = cadastrosTotal,
|
||||||
|
["travel_com"] = travelCom,
|
||||||
|
["adicional_pago"] = adicionalPago,
|
||||||
|
["planos_contratados"] = planosContratados,
|
||||||
|
["usuarios_com_linha"] = usuariosComLinha
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Guid? TryParseUserId(ClaimsPrincipal user)
|
||||||
|
{
|
||||||
|
var claim = user.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
||||||
|
return Guid.TryParse(claim, out var userId) ? userId : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IEnumerable<(string Scope, string? Operadora)> EnumerateScopes()
|
||||||
|
{
|
||||||
|
yield return ("TODOS", null);
|
||||||
|
yield return ("VIVO", "VIVO");
|
||||||
|
yield return ("CLARO", "CLARO");
|
||||||
|
yield return ("TIM", "TIM");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string NormalizeScope(string? operadora)
|
||||||
|
{
|
||||||
|
var token = NormalizeToken(operadora);
|
||||||
|
if (string.IsNullOrWhiteSpace(token))
|
||||||
|
{
|
||||||
|
return "TODOS";
|
||||||
|
}
|
||||||
|
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DashboardKpiSnapshotPayload? DeserializeSnapshot(string? metadataJson)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(metadataJson))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return JsonSerializer.Deserialize<DashboardKpiSnapshotPayload>(metadataJson, JsonOptions);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IQueryable<MobileLine> ApplyReservaContextFilter(IQueryable<MobileLine> query)
|
||||||
|
{
|
||||||
|
return query.Where(x =>
|
||||||
|
EF.Functions.ILike((x.Usuario ?? "").Trim(), "RESERVA") ||
|
||||||
|
EF.Functions.ILike((x.Skil ?? "").Trim(), "RESERVA") ||
|
||||||
|
EF.Functions.ILike((x.Cliente ?? "").Trim(), "RESERVA"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IQueryable<MobileLine> ExcludeReservaContext(IQueryable<MobileLine> query)
|
||||||
|
{
|
||||||
|
return query.Where(x =>
|
||||||
|
!EF.Functions.ILike((x.Usuario ?? "").Trim(), "RESERVA") &&
|
||||||
|
!EF.Functions.ILike((x.Skil ?? "").Trim(), "RESERVA") &&
|
||||||
|
!EF.Functions.ILike((x.Cliente ?? "").Trim(), "RESERVA"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool ShouldIncludeUsuario(string usuario, string status)
|
||||||
|
{
|
||||||
|
var usuarioKey = NormalizeToken(usuario);
|
||||||
|
if (string.IsNullOrWhiteSpace(usuarioKey))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var invalidUserTokens = new[]
|
||||||
|
{
|
||||||
|
"SEMUSUARIO",
|
||||||
|
"AGUARDANDOUSUARIO",
|
||||||
|
"AGUARDANDO",
|
||||||
|
"BLOQUEAR",
|
||||||
|
"BLOQUEAD",
|
||||||
|
"BLOQUEADO",
|
||||||
|
"BLOQUEIO",
|
||||||
|
"BLOQ120",
|
||||||
|
"RESERVA",
|
||||||
|
"NAOATRIBUIDO",
|
||||||
|
"PENDENTE",
|
||||||
|
"COBRANCA",
|
||||||
|
"FATURAMENTO",
|
||||||
|
"FINANCEIRO",
|
||||||
|
"BACKOFFICE",
|
||||||
|
"ADMINISTRATIVO",
|
||||||
|
};
|
||||||
|
|
||||||
|
if (invalidUserTokens.Any(usuarioKey.Contains))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var statusKey = NormalizeToken(status);
|
||||||
|
var blockedStatusTokens = new[] { "BLOQUE", "PERDA", "ROUBO", "SUSPEN", "CANCEL", "AGUARD" };
|
||||||
|
return !blockedStatusTokens.Any(statusKey.Contains);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string NormalizeToken(string? value)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
var normalized = value.Normalize(NormalizationForm.FormD);
|
||||||
|
var builder = new StringBuilder(normalized.Length);
|
||||||
|
foreach (var ch in normalized)
|
||||||
|
{
|
||||||
|
var category = CharUnicodeInfo.GetUnicodeCategory(ch);
|
||||||
|
if (category == UnicodeCategory.NonSpacingMark)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (char.IsLetterOrDigit(ch))
|
||||||
|
{
|
||||||
|
builder.Append(char.ToUpperInvariant(ch));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class DashboardKpiSnapshotPayload
|
||||||
|
{
|
||||||
|
public string Scope { get; set; } = "TODOS";
|
||||||
|
public DateTime CapturedAtUtc { get; set; }
|
||||||
|
public Dictionary<string, decimal> Metrics { get; set; } = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -25,9 +25,9 @@ namespace line_gestao_api.Services
|
||||||
_db = db;
|
_db = db;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<GeralDashboardInsightsDto> GetInsightsAsync()
|
public async Task<GeralDashboardInsightsDto> GetInsightsAsync(string? operadora = null)
|
||||||
{
|
{
|
||||||
var qLines = _db.MobileLines.AsNoTracking();
|
var qLines = OperadoraContaResolver.ApplyOperadoraFilter(_db.MobileLines.AsNoTracking(), operadora);
|
||||||
|
|
||||||
var totals = await qLines
|
var totals = await qLines
|
||||||
.GroupBy(_ => 1)
|
.GroupBy(_ => 1)
|
||||||
|
|
@ -99,6 +99,7 @@ namespace line_gestao_api.Services
|
||||||
x.VivoGestaoDispositivo != null)
|
x.VivoGestaoDispositivo != null)
|
||||||
? (x.FranquiaVivo ?? 0m)
|
? (x.FranquiaVivo ?? 0m)
|
||||||
: 0m),
|
: 0m),
|
||||||
|
TotalFranquiaLine = g.Sum(x => x.FranquiaLine ?? 0m),
|
||||||
VivoAdicionaisTotal = g.Sum(x =>
|
VivoAdicionaisTotal = g.Sum(x =>
|
||||||
(x.ValorPlanoVivo != null ||
|
(x.ValorPlanoVivo != null ||
|
||||||
x.FranquiaVivo != null ||
|
x.FranquiaVivo != null ||
|
||||||
|
|
@ -653,6 +654,7 @@ namespace line_gestao_api.Services
|
||||||
{
|
{
|
||||||
QtdLinhas = totals.VivoLinhas,
|
QtdLinhas = totals.VivoLinhas,
|
||||||
TotalFranquiaGb = totals.VivoFranquiaTotalGb,
|
TotalFranquiaGb = totals.VivoFranquiaTotalGb,
|
||||||
|
TotalFranquiaLine = totals.TotalFranquiaLine,
|
||||||
TotalBaseMensal = totals.VivoBaseTotal,
|
TotalBaseMensal = totals.VivoBaseTotal,
|
||||||
TotalAdicionaisMensal = totals.VivoAdicionaisTotal,
|
TotalAdicionaisMensal = totals.VivoAdicionaisTotal,
|
||||||
TotalGeralMensal = totalGeralMensal,
|
TotalGeralMensal = totalGeralMensal,
|
||||||
|
|
@ -1110,6 +1112,7 @@ namespace line_gestao_api.Services
|
||||||
public int TotalBloqueados { get; set; }
|
public int TotalBloqueados { get; set; }
|
||||||
public int VivoLinhas { get; set; }
|
public int VivoLinhas { get; set; }
|
||||||
public decimal VivoFranquiaTotalGb { get; set; }
|
public decimal VivoFranquiaTotalGb { get; set; }
|
||||||
|
public decimal TotalFranquiaLine { get; set; }
|
||||||
public decimal VivoBaseTotal { get; set; }
|
public decimal VivoBaseTotal { get; set; }
|
||||||
public decimal VivoAdicionaisTotal { get; set; }
|
public decimal VivoAdicionaisTotal { get; set; }
|
||||||
public decimal VivoMinBase { get; set; }
|
public decimal VivoMinBase { get; set; }
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,323 @@
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace line_gestao_api.Services;
|
||||||
|
|
||||||
|
internal static class MveAuditNormalization
|
||||||
|
{
|
||||||
|
public static string NormalizeHeader(string? value)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
var normalized = value.Trim().ToUpperInvariant().Normalize(NormalizationForm.FormD);
|
||||||
|
var builder = new StringBuilder(normalized.Length);
|
||||||
|
foreach (var ch in normalized)
|
||||||
|
{
|
||||||
|
if (CharUnicodeInfo.GetUnicodeCategory(ch) != UnicodeCategory.NonSpacingMark)
|
||||||
|
{
|
||||||
|
builder.Append(ch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.ToString()
|
||||||
|
.Replace("\u00A0", " ")
|
||||||
|
.Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string CleanTextValue(string? value, bool removeSingleQuotes = true)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
var cleaned = value
|
||||||
|
.Replace("\u00A0", " ")
|
||||||
|
.Replace("\t", " ")
|
||||||
|
.Replace("\r", " ")
|
||||||
|
.Replace("\n", " ")
|
||||||
|
.Trim();
|
||||||
|
|
||||||
|
if (removeSingleQuotes)
|
||||||
|
{
|
||||||
|
cleaned = cleaned.Replace("'", string.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
while (cleaned.Contains(" ", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
cleaned = cleaned.Replace(" ", " ", StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
return cleaned.Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string OnlyDigits(string? value)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
var builder = new StringBuilder(value.Length);
|
||||||
|
foreach (var ch in value)
|
||||||
|
{
|
||||||
|
if (char.IsDigit(ch))
|
||||||
|
{
|
||||||
|
builder.Append(ch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string? NullIfEmptyDigits(string? value)
|
||||||
|
{
|
||||||
|
var digits = OnlyDigits(value);
|
||||||
|
return string.IsNullOrWhiteSpace(digits) ? null : digits;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string NormalizePhoneDigits(string? value)
|
||||||
|
{
|
||||||
|
var digits = OnlyDigits(value);
|
||||||
|
if (string.IsNullOrWhiteSpace(digits))
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (digits.StartsWith("55", StringComparison.Ordinal) && digits.Length is 12 or 13)
|
||||||
|
{
|
||||||
|
digits = digits[2..];
|
||||||
|
}
|
||||||
|
|
||||||
|
return digits.Length > 11 ? digits[^11..] : digits;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string ExtractPhoneDdd(string? value)
|
||||||
|
{
|
||||||
|
var digits = NormalizePhoneDigits(value);
|
||||||
|
return digits.Length >= 10 ? digits[..2] : string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string ExtractPhoneLocalNumber(string? value)
|
||||||
|
{
|
||||||
|
var digits = NormalizePhoneDigits(value);
|
||||||
|
return digits.Length >= 10 ? digits[2..] : digits;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string NormalizeComparableText(string? value)
|
||||||
|
{
|
||||||
|
var cleaned = CleanTextValue(value);
|
||||||
|
if (string.IsNullOrWhiteSpace(cleaned))
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
return NormalizeHeader(cleaned)
|
||||||
|
.Replace(" ", " ", StringComparison.Ordinal)
|
||||||
|
.Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string NormalizeAccountLike(string? value)
|
||||||
|
{
|
||||||
|
var digits = OnlyDigits(value);
|
||||||
|
if (!string.IsNullOrWhiteSpace(digits))
|
||||||
|
{
|
||||||
|
return digits;
|
||||||
|
}
|
||||||
|
|
||||||
|
return NormalizeComparableText(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static DateTime? ParseDateValue(string? rawValue)
|
||||||
|
{
|
||||||
|
var cleaned = CleanTextValue(rawValue);
|
||||||
|
if (string.IsNullOrWhiteSpace(cleaned))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (double.TryParse(
|
||||||
|
cleaned.Replace(",", ".", StringComparison.Ordinal),
|
||||||
|
NumberStyles.Float,
|
||||||
|
CultureInfo.InvariantCulture,
|
||||||
|
out var oaValue) &&
|
||||||
|
oaValue > 10_000 &&
|
||||||
|
oaValue < 90_000)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return ToUtcDateOnly(DateTime.FromOADate(oaValue));
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// segue para os demais formatos
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var formats = new[]
|
||||||
|
{
|
||||||
|
"dd/MM/yyyy",
|
||||||
|
"d/M/yyyy",
|
||||||
|
"dd/MM/yy",
|
||||||
|
"d/M/yy",
|
||||||
|
"yyyy-MM-dd",
|
||||||
|
"dd-MM-yyyy",
|
||||||
|
"d-M-yyyy",
|
||||||
|
"yyyyMMdd"
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var format in formats)
|
||||||
|
{
|
||||||
|
if (DateTime.TryParseExact(
|
||||||
|
cleaned,
|
||||||
|
format,
|
||||||
|
CultureInfo.InvariantCulture,
|
||||||
|
DateTimeStyles.None,
|
||||||
|
out var exact))
|
||||||
|
{
|
||||||
|
return ToUtcDateOnly(exact);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (DateTime.TryParse(cleaned, new CultureInfo("pt-BR"), DateTimeStyles.None, out var parsedBr))
|
||||||
|
{
|
||||||
|
return ToUtcDateOnly(parsedBr);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (DateTime.TryParse(cleaned, CultureInfo.InvariantCulture, DateTimeStyles.None, out var parsedInvariant))
|
||||||
|
{
|
||||||
|
return ToUtcDateOnly(parsedInvariant);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static DateTime ToUtcDateOnly(DateTime date)
|
||||||
|
{
|
||||||
|
return new DateTime(date.Year, date.Month, date.Day, 12, 0, 0, DateTimeKind.Utc);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string FormatDate(DateTime? value)
|
||||||
|
{
|
||||||
|
return value.HasValue ? value.Value.ToString("dd/MM/yyyy", CultureInfo.InvariantCulture) : string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static MveNormalizedStatus NormalizeReportStatus(string? rawValue)
|
||||||
|
{
|
||||||
|
var displayValue = CleanTextValue(rawValue);
|
||||||
|
if (string.IsNullOrWhiteSpace(displayValue))
|
||||||
|
{
|
||||||
|
return new MveNormalizedStatus(string.Empty, string.Empty, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
var headSegment = NormalizeComparableText(displayValue.Split(':', 2)[0])
|
||||||
|
.Replace("/", " ", StringComparison.Ordinal)
|
||||||
|
.Replace("-", " ", StringComparison.Ordinal)
|
||||||
|
.Replace(".", " ", StringComparison.Ordinal)
|
||||||
|
.Replace("(", " ", StringComparison.Ordinal)
|
||||||
|
.Replace(")", " ", StringComparison.Ordinal)
|
||||||
|
.Replace(" ", " ", StringComparison.Ordinal)
|
||||||
|
.Trim();
|
||||||
|
|
||||||
|
var canonical = NormalizeComparableText(displayValue)
|
||||||
|
.Replace("/", " ", StringComparison.Ordinal)
|
||||||
|
.Replace("-", " ", StringComparison.Ordinal)
|
||||||
|
.Replace(".", " ", StringComparison.Ordinal)
|
||||||
|
.Replace("(", " ", StringComparison.Ordinal)
|
||||||
|
.Replace(")", " ", StringComparison.Ordinal)
|
||||||
|
.Replace(" ", " ", StringComparison.Ordinal)
|
||||||
|
.Trim();
|
||||||
|
|
||||||
|
var key = headSegment switch
|
||||||
|
{
|
||||||
|
var text when text.Contains("ATIVO", StringComparison.Ordinal) || text == "ATIVA" => "ATIVO",
|
||||||
|
var text when text.Contains("BLOQUEIO PARCIAL", StringComparison.Ordinal) => "BLOQUEIO_PERDA_ROUBO",
|
||||||
|
var text when text.Contains("PRE ATIVACAO", StringComparison.Ordinal) => "BLOQUEIO_PERDA_ROUBO",
|
||||||
|
var text when text.Contains("CANCEL", StringComparison.Ordinal) => "BLOQUEIO_120_DIAS",
|
||||||
|
var text when text.Contains("SUSPENS", StringComparison.Ordinal) => "SUSPENSO",
|
||||||
|
var text when text.Contains("PENDENTE", StringComparison.Ordinal) &&
|
||||||
|
text.Contains("TROCA", StringComparison.Ordinal) &&
|
||||||
|
text.Contains("NUMERO", StringComparison.Ordinal) => "PENDENTE_TROCA_NUMERO",
|
||||||
|
var text when text.Contains("PERDA", StringComparison.Ordinal) || text.Contains("ROUBO", StringComparison.Ordinal) => "BLOQUEIO_PERDA_ROUBO",
|
||||||
|
var text when text.Contains("BLOQUEIO", StringComparison.Ordinal) && text.Contains("120", StringComparison.Ordinal) => "BLOQUEIO_120_DIAS",
|
||||||
|
_ => canonical.Replace(" ", "_", StringComparison.Ordinal)
|
||||||
|
};
|
||||||
|
|
||||||
|
var recognized = key is
|
||||||
|
"ATIVO" or
|
||||||
|
"BLOQUEIO_PERDA_ROUBO" or
|
||||||
|
"BLOQUEIO_120_DIAS" or
|
||||||
|
"SUSPENSO" or
|
||||||
|
"PENDENTE_TROCA_NUMERO";
|
||||||
|
|
||||||
|
return new MveNormalizedStatus(displayValue, key, recognized);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static MveNormalizedStatus NormalizeSystemStatus(string? rawValue)
|
||||||
|
{
|
||||||
|
var displayValue = CleanTextValue(rawValue);
|
||||||
|
if (string.IsNullOrWhiteSpace(displayValue))
|
||||||
|
{
|
||||||
|
return new MveNormalizedStatus(string.Empty, string.Empty, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
var canonical = NormalizeComparableText(displayValue)
|
||||||
|
.Replace("/", " ", StringComparison.Ordinal)
|
||||||
|
.Replace("-", " ", StringComparison.Ordinal)
|
||||||
|
.Replace(".", " ", StringComparison.Ordinal)
|
||||||
|
.Replace(":", " ", StringComparison.Ordinal)
|
||||||
|
.Replace("(", " ", StringComparison.Ordinal)
|
||||||
|
.Replace(")", " ", StringComparison.Ordinal)
|
||||||
|
.Replace(" ", " ", StringComparison.Ordinal)
|
||||||
|
.Trim();
|
||||||
|
|
||||||
|
var key = canonical switch
|
||||||
|
{
|
||||||
|
var text when text.Contains("ATIVO", StringComparison.Ordinal) || text == "ATIVA" => "ATIVO",
|
||||||
|
var text when text.Contains("PERDA", StringComparison.Ordinal) || text.Contains("ROUBO", StringComparison.Ordinal) => "BLOQUEIO_PERDA_ROUBO",
|
||||||
|
var text when text.Contains("BLOQUEIO PARCIAL", StringComparison.Ordinal) => "BLOQUEIO_PERDA_ROUBO",
|
||||||
|
var text when text.Contains("PRE ATIVACAO", StringComparison.Ordinal) => "BLOQUEIO_PERDA_ROUBO",
|
||||||
|
var text when text.Contains("BLOQUEIO", StringComparison.Ordinal) && text.Contains("120", StringComparison.Ordinal) => "BLOQUEIO_120_DIAS",
|
||||||
|
var text when text.Contains("CANCEL", StringComparison.Ordinal) => "CANCELADO",
|
||||||
|
var text when text.Contains("SUSPENS", StringComparison.Ordinal) => "SUSPENSO",
|
||||||
|
var text when text.Contains("PENDENTE", StringComparison.Ordinal) &&
|
||||||
|
text.Contains("TROCA", StringComparison.Ordinal) &&
|
||||||
|
text.Contains("NUMERO", StringComparison.Ordinal) => "PENDENTE_TROCA_NUMERO",
|
||||||
|
_ => canonical.Replace(" ", "_", StringComparison.Ordinal)
|
||||||
|
};
|
||||||
|
|
||||||
|
var recognized = key is
|
||||||
|
"ATIVO" or
|
||||||
|
"BLOQUEIO_PERDA_ROUBO" or
|
||||||
|
"BLOQUEIO_120_DIAS" or
|
||||||
|
"CANCELADO" or
|
||||||
|
"SUSPENSO" or
|
||||||
|
"PENDENTE_TROCA_NUMERO";
|
||||||
|
|
||||||
|
return new MveNormalizedStatus(displayValue, key, recognized);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static MveNormalizedStatus NormalizeStatus(string? rawValue)
|
||||||
|
{
|
||||||
|
return NormalizeSystemStatus(rawValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string NormalizeStatusForSystem(string? rawValue)
|
||||||
|
{
|
||||||
|
var normalized = NormalizeReportStatus(rawValue);
|
||||||
|
return normalized.Key switch
|
||||||
|
{
|
||||||
|
"ATIVO" => "ATIVO",
|
||||||
|
"BLOQUEIO_PERDA_ROUBO" => "BLOQUEIO PERDA/ROUBO",
|
||||||
|
"BLOQUEIO_120_DIAS" => "BLOQUEIO 120 DIAS",
|
||||||
|
"SUSPENSO" => "SUSPENSO",
|
||||||
|
"PENDENTE_TROCA_NUMERO" => "PENDENTE TROCA NUMERO",
|
||||||
|
_ => normalized.DisplayValue
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed record MveNormalizedStatus(string DisplayValue, string Key, bool Recognized);
|
||||||
|
|
@ -0,0 +1,134 @@
|
||||||
|
using line_gestao_api.Data;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace line_gestao_api.Services;
|
||||||
|
|
||||||
|
public sealed class MveAuditSchemaBootstrapper
|
||||||
|
{
|
||||||
|
private readonly AppDbContext _db;
|
||||||
|
|
||||||
|
public MveAuditSchemaBootstrapper(AppDbContext db)
|
||||||
|
{
|
||||||
|
_db = db;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task EnsureSchemaAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (!_db.Database.IsRelational())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _db.Database.ExecuteSqlRawAsync(
|
||||||
|
"""
|
||||||
|
ALTER TABLE "Aparelhos"
|
||||||
|
ADD COLUMN IF NOT EXISTS "Fabricante" character varying(120) NULL;
|
||||||
|
""",
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
await _db.Database.ExecuteSqlRawAsync(
|
||||||
|
"""
|
||||||
|
CREATE INDEX IF NOT EXISTS "IX_Aparelhos_TenantId_Fabricante"
|
||||||
|
ON "Aparelhos" ("TenantId", "Fabricante");
|
||||||
|
""",
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
await _db.Database.ExecuteSqlRawAsync(
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS "MveAuditRuns" (
|
||||||
|
"Id" uuid NOT NULL,
|
||||||
|
"TenantId" uuid NOT NULL,
|
||||||
|
"FileName" character varying(260) NULL,
|
||||||
|
"FileHashSha256" character varying(64) NULL,
|
||||||
|
"FileEncoding" character varying(40) NULL,
|
||||||
|
"Status" character varying(40) NOT NULL,
|
||||||
|
"TotalSystemLines" integer NOT NULL,
|
||||||
|
"TotalReportLines" integer NOT NULL,
|
||||||
|
"TotalConciliated" integer NOT NULL,
|
||||||
|
"TotalStatusDivergences" integer NOT NULL,
|
||||||
|
"TotalDataDivergences" integer NOT NULL,
|
||||||
|
"TotalOnlyInSystem" integer NOT NULL,
|
||||||
|
"TotalOnlyInReport" integer NOT NULL,
|
||||||
|
"TotalDuplicateReportLines" integer NOT NULL,
|
||||||
|
"TotalDuplicateSystemLines" integer NOT NULL,
|
||||||
|
"TotalInvalidRows" integer NOT NULL,
|
||||||
|
"TotalUnknownStatuses" integer NOT NULL,
|
||||||
|
"TotalSyncableIssues" integer NOT NULL,
|
||||||
|
"AppliedIssuesCount" integer NOT NULL,
|
||||||
|
"AppliedLinesCount" integer NOT NULL,
|
||||||
|
"AppliedFieldsCount" integer NOT NULL,
|
||||||
|
"ImportedAtUtc" timestamp with time zone NOT NULL,
|
||||||
|
"AppliedAtUtc" timestamp with time zone NULL,
|
||||||
|
"AppliedByUserId" uuid NULL,
|
||||||
|
"AppliedByUserName" character varying(200) NULL,
|
||||||
|
"AppliedByUserEmail" character varying(200) NULL,
|
||||||
|
CONSTRAINT "PK_MveAuditRuns" PRIMARY KEY ("Id")
|
||||||
|
);
|
||||||
|
""",
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
await _db.Database.ExecuteSqlRawAsync(
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS "MveAuditIssues" (
|
||||||
|
"Id" uuid NOT NULL,
|
||||||
|
"TenantId" uuid NOT NULL,
|
||||||
|
"AuditRunId" uuid NOT NULL,
|
||||||
|
"SourceRowNumber" integer NULL,
|
||||||
|
"NumeroLinha" character varying(64) NOT NULL,
|
||||||
|
"MobileLineId" uuid NULL,
|
||||||
|
"SystemItem" integer NULL,
|
||||||
|
"IssueType" character varying(60) NOT NULL,
|
||||||
|
"Situation" character varying(80) NOT NULL,
|
||||||
|
"Severity" character varying(40) NOT NULL,
|
||||||
|
"Syncable" boolean NOT NULL,
|
||||||
|
"Applied" boolean NOT NULL,
|
||||||
|
"AppliedAtUtc" timestamp with time zone NULL,
|
||||||
|
"ActionSuggestion" character varying(160) NULL,
|
||||||
|
"Notes" character varying(500) NULL,
|
||||||
|
"SystemStatus" character varying(120) NULL,
|
||||||
|
"ReportStatus" character varying(120) NULL,
|
||||||
|
"SystemPlan" character varying(200) NULL,
|
||||||
|
"ReportPlan" character varying(200) NULL,
|
||||||
|
"SystemSnapshotJson" jsonb NOT NULL,
|
||||||
|
"ReportSnapshotJson" jsonb NOT NULL,
|
||||||
|
"DifferencesJson" jsonb NOT NULL,
|
||||||
|
"CreatedAtUtc" timestamp with time zone NOT NULL,
|
||||||
|
CONSTRAINT "PK_MveAuditIssues" PRIMARY KEY ("Id")
|
||||||
|
);
|
||||||
|
""",
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
await _db.Database.ExecuteSqlRawAsync(
|
||||||
|
"""
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM pg_constraint
|
||||||
|
WHERE conname = 'FK_MveAuditIssues_MveAuditRuns_AuditRunId'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE "MveAuditIssues"
|
||||||
|
ADD CONSTRAINT "FK_MveAuditIssues_MveAuditRuns_AuditRunId"
|
||||||
|
FOREIGN KEY ("AuditRunId")
|
||||||
|
REFERENCES "MveAuditRuns" ("Id")
|
||||||
|
ON DELETE CASCADE;
|
||||||
|
END IF;
|
||||||
|
END
|
||||||
|
$$;
|
||||||
|
""",
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
await _db.Database.ExecuteSqlRawAsync(
|
||||||
|
"""
|
||||||
|
CREATE INDEX IF NOT EXISTS "IX_MveAuditIssues_AuditRunId" ON "MveAuditIssues" ("AuditRunId");
|
||||||
|
CREATE INDEX IF NOT EXISTS "IX_MveAuditIssues_IssueType" ON "MveAuditIssues" ("IssueType");
|
||||||
|
CREATE INDEX IF NOT EXISTS "IX_MveAuditIssues_NumeroLinha" ON "MveAuditIssues" ("NumeroLinha");
|
||||||
|
CREATE INDEX IF NOT EXISTS "IX_MveAuditIssues_Syncable" ON "MveAuditIssues" ("Syncable");
|
||||||
|
CREATE INDEX IF NOT EXISTS "IX_MveAuditIssues_TenantId" ON "MveAuditIssues" ("TenantId");
|
||||||
|
CREATE INDEX IF NOT EXISTS "IX_MveAuditRuns_ImportedAtUtc" ON "MveAuditRuns" ("ImportedAtUtc");
|
||||||
|
CREATE INDEX IF NOT EXISTS "IX_MveAuditRuns_Status" ON "MveAuditRuns" ("Status");
|
||||||
|
CREATE INDEX IF NOT EXISTS "IX_MveAuditRuns_TenantId" ON "MveAuditRuns" ("TenantId");
|
||||||
|
""",
|
||||||
|
cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,818 @@
|
||||||
|
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.Dtos;
|
||||||
|
using line_gestao_api.Models;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace line_gestao_api.Services;
|
||||||
|
|
||||||
|
public sealed class MveAuditService
|
||||||
|
{
|
||||||
|
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||||
|
{
|
||||||
|
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
|
||||||
|
};
|
||||||
|
|
||||||
|
private readonly AppDbContext _db;
|
||||||
|
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||||
|
private readonly ITenantProvider _tenantProvider;
|
||||||
|
private readonly MveCsvParserService _parser;
|
||||||
|
private readonly MveReconciliationService _reconciliation;
|
||||||
|
private readonly IVigenciaNotificationSyncService _vigenciaNotificationSyncService;
|
||||||
|
|
||||||
|
public MveAuditService(
|
||||||
|
AppDbContext db,
|
||||||
|
IHttpContextAccessor httpContextAccessor,
|
||||||
|
ITenantProvider tenantProvider,
|
||||||
|
MveCsvParserService parser,
|
||||||
|
MveReconciliationService reconciliation,
|
||||||
|
IVigenciaNotificationSyncService vigenciaNotificationSyncService)
|
||||||
|
{
|
||||||
|
_db = db;
|
||||||
|
_httpContextAccessor = httpContextAccessor;
|
||||||
|
_tenantProvider = tenantProvider;
|
||||||
|
_parser = parser;
|
||||||
|
_reconciliation = reconciliation;
|
||||||
|
_vigenciaNotificationSyncService = vigenciaNotificationSyncService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<MveAuditRunDto> CreateRunAsync(IFormFile file, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
ValidateInputFile(file);
|
||||||
|
|
||||||
|
var parsedFile = await _parser.ParseAsync(file, cancellationToken);
|
||||||
|
var reconciliation = await _reconciliation.BuildAsync(parsedFile, cancellationToken);
|
||||||
|
|
||||||
|
var run = new MveAuditRun
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
FileName = parsedFile.FileName,
|
||||||
|
FileHashSha256 = parsedFile.FileHashSha256,
|
||||||
|
FileEncoding = parsedFile.FileEncoding,
|
||||||
|
Status = "READY",
|
||||||
|
TotalSystemLines = reconciliation.TotalSystemLines,
|
||||||
|
TotalReportLines = reconciliation.TotalReportLines,
|
||||||
|
TotalConciliated = reconciliation.TotalConciliated,
|
||||||
|
TotalStatusDivergences = reconciliation.TotalStatusDivergences,
|
||||||
|
TotalDataDivergences = reconciliation.TotalDataDivergences,
|
||||||
|
TotalOnlyInSystem = reconciliation.TotalOnlyInSystem,
|
||||||
|
TotalOnlyInReport = reconciliation.TotalOnlyInReport,
|
||||||
|
TotalDuplicateReportLines = reconciliation.TotalDuplicateReportLines,
|
||||||
|
TotalDuplicateSystemLines = reconciliation.TotalDuplicateSystemLines,
|
||||||
|
TotalInvalidRows = reconciliation.TotalInvalidRows,
|
||||||
|
TotalUnknownStatuses = reconciliation.TotalUnknownStatuses,
|
||||||
|
TotalSyncableIssues = reconciliation.TotalSyncableIssues,
|
||||||
|
ImportedAtUtc = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var issue in reconciliation.Issues)
|
||||||
|
{
|
||||||
|
run.Issues.Add(new MveAuditIssue
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
AuditRunId = run.Id,
|
||||||
|
SourceRowNumber = issue.SourceRowNumber,
|
||||||
|
NumeroLinha = string.IsNullOrWhiteSpace(issue.NumeroLinha) ? "-" : issue.NumeroLinha.Trim(),
|
||||||
|
MobileLineId = issue.MobileLineId,
|
||||||
|
SystemItem = issue.SystemItem,
|
||||||
|
IssueType = issue.IssueType,
|
||||||
|
Situation = issue.Situation,
|
||||||
|
Severity = issue.Severity,
|
||||||
|
Syncable = issue.Syncable,
|
||||||
|
ActionSuggestion = issue.ActionSuggestion,
|
||||||
|
Notes = issue.Notes,
|
||||||
|
SystemStatus = issue.SystemStatus,
|
||||||
|
ReportStatus = issue.ReportStatus,
|
||||||
|
SystemPlan = issue.SystemPlan,
|
||||||
|
ReportPlan = issue.ReportPlan,
|
||||||
|
SystemSnapshotJson = JsonSerializer.Serialize(issue.SystemSnapshot, JsonOptions),
|
||||||
|
ReportSnapshotJson = JsonSerializer.Serialize(issue.ReportSnapshot, JsonOptions),
|
||||||
|
DifferencesJson = JsonSerializer.Serialize(issue.Differences, JsonOptions),
|
||||||
|
CreatedAtUtc = DateTime.UtcNow
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_db.MveAuditRuns.Add(run);
|
||||||
|
_db.AuditLogs.Add(BuildAuditLog(
|
||||||
|
action: "MVE_AUDIT_RUN",
|
||||||
|
runId: run.Id,
|
||||||
|
fileName: run.FileName,
|
||||||
|
changes: new List<AuditFieldChangeDto>
|
||||||
|
{
|
||||||
|
new() { Field = "TotalLinhasSistema", ChangeType = "captured", NewValue = run.TotalSystemLines.ToString() },
|
||||||
|
new() { Field = "TotalLinhasRelatorio", ChangeType = "captured", NewValue = run.TotalReportLines.ToString() },
|
||||||
|
new() { Field = "DivergenciasStatus", ChangeType = "captured", NewValue = run.TotalStatusDivergences.ToString() },
|
||||||
|
new() { Field = "DivergenciasCadastro", ChangeType = "captured", NewValue = run.TotalDataDivergences.ToString() },
|
||||||
|
new() { Field = "ItensSincronizaveis", ChangeType = "captured", NewValue = run.TotalSyncableIssues.ToString() }
|
||||||
|
},
|
||||||
|
metadata: new
|
||||||
|
{
|
||||||
|
run.FileHashSha256,
|
||||||
|
run.FileEncoding,
|
||||||
|
run.TotalOnlyInSystem,
|
||||||
|
run.TotalOnlyInReport,
|
||||||
|
run.TotalDuplicateReportLines,
|
||||||
|
run.TotalDuplicateSystemLines,
|
||||||
|
run.TotalInvalidRows,
|
||||||
|
parsedFile.SourceRowCount
|
||||||
|
}));
|
||||||
|
|
||||||
|
await _db.SaveChangesAsync(cancellationToken);
|
||||||
|
return ToDto(run);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<MveAuditRunDto?> GetByIdAsync(Guid runId, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var run = await _db.MveAuditRuns
|
||||||
|
.AsNoTracking()
|
||||||
|
.Include(x => x.Issues)
|
||||||
|
.FirstOrDefaultAsync(x => x.Id == runId, cancellationToken);
|
||||||
|
|
||||||
|
return run == null ? null : ToDto(run);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<MveAuditRunDto?> GetLatestAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var run = await _db.MveAuditRuns
|
||||||
|
.AsNoTracking()
|
||||||
|
.Include(x => x.Issues)
|
||||||
|
.OrderByDescending(x => x.ImportedAtUtc)
|
||||||
|
.ThenByDescending(x => x.Id)
|
||||||
|
.FirstOrDefaultAsync(cancellationToken);
|
||||||
|
|
||||||
|
return run == null ? null : ToDto(run);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ApplyMveAuditResultDto?> ApplyAsync(
|
||||||
|
Guid runId,
|
||||||
|
IReadOnlyCollection<Guid>? issueIds,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var run = await _db.MveAuditRuns
|
||||||
|
.Include(x => x.Issues)
|
||||||
|
.FirstOrDefaultAsync(x => x.Id == runId, cancellationToken);
|
||||||
|
|
||||||
|
if (run == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var requestedIds = issueIds?
|
||||||
|
.Where(x => x != Guid.Empty)
|
||||||
|
.Distinct()
|
||||||
|
.ToHashSet()
|
||||||
|
?? new HashSet<Guid>();
|
||||||
|
|
||||||
|
var selectedIssues = run.Issues
|
||||||
|
.Where(x => x.Syncable && !x.Applied)
|
||||||
|
.Where(x => requestedIds.Count == 0 || requestedIds.Contains(x.Id))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var result = new ApplyMveAuditResultDto
|
||||||
|
{
|
||||||
|
AuditRunId = run.Id,
|
||||||
|
RequestedIssues = requestedIds.Count == 0 ? selectedIssues.Count : requestedIds.Count
|
||||||
|
};
|
||||||
|
|
||||||
|
if (selectedIssues.Count == 0)
|
||||||
|
{
|
||||||
|
result.SkippedIssues = result.RequestedIssues;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
var lineIds = selectedIssues
|
||||||
|
.Where(x => x.MobileLineId.HasValue)
|
||||||
|
.Select(x => x.MobileLineId!.Value)
|
||||||
|
.Distinct()
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var linesById = await _db.MobileLines
|
||||||
|
.Where(x => lineIds.Contains(x.Id))
|
||||||
|
.ToDictionaryAsync(x => x.Id, cancellationToken);
|
||||||
|
|
||||||
|
var reportSnapshotsByIssueId = selectedIssues
|
||||||
|
.ToDictionary(x => x.Id, x => DeserializeSnapshot(x.ReportSnapshotJson));
|
||||||
|
|
||||||
|
var lineNumbers = linesById.Values
|
||||||
|
.Select(x => MveAuditNormalization.NullIfEmptyDigits(x.Linha))
|
||||||
|
.Where(x => !string.IsNullOrWhiteSpace(x))
|
||||||
|
.Cast<string>()
|
||||||
|
.Concat(reportSnapshotsByIssueId.Values
|
||||||
|
.Select(x => MveAuditNormalization.NullIfEmptyDigits(x?.NumeroLinha))
|
||||||
|
.Where(x => !string.IsNullOrWhiteSpace(x))
|
||||||
|
.Cast<string>())
|
||||||
|
.Distinct(StringComparer.Ordinal)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var items = linesById.Values
|
||||||
|
.Where(x => x.Item > 0)
|
||||||
|
.Select(x => x.Item)
|
||||||
|
.Distinct()
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var vigencias = await _db.VigenciaLines
|
||||||
|
.Where(x => (x.Item > 0 && items.Contains(x.Item)) || (x.Linha != null && lineNumbers.Contains(x.Linha)))
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
var userDatas = await _db.UserDatas
|
||||||
|
.Where(x => (x.Item > 0 && items.Contains(x.Item)) || (x.Linha != null && lineNumbers.Contains(x.Linha)))
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
var vigenciaByLine = vigencias
|
||||||
|
.Where(x => !string.IsNullOrWhiteSpace(x.Linha))
|
||||||
|
.GroupBy(x => x.Linha!, StringComparer.Ordinal)
|
||||||
|
.ToDictionary(
|
||||||
|
g => g.Key,
|
||||||
|
g => g.OrderByDescending(x => x.UpdatedAt).ThenByDescending(x => x.CreatedAt).First(),
|
||||||
|
StringComparer.Ordinal);
|
||||||
|
|
||||||
|
var vigenciaByItem = vigencias
|
||||||
|
.Where(x => x.Item > 0)
|
||||||
|
.GroupBy(x => x.Item)
|
||||||
|
.ToDictionary(
|
||||||
|
g => g.Key,
|
||||||
|
g => g.OrderByDescending(x => x.UpdatedAt).ThenByDescending(x => x.CreatedAt).First());
|
||||||
|
|
||||||
|
var userDataByLine = userDatas
|
||||||
|
.Where(x => !string.IsNullOrWhiteSpace(x.Linha))
|
||||||
|
.GroupBy(x => x.Linha!, StringComparer.Ordinal)
|
||||||
|
.ToDictionary(
|
||||||
|
g => g.Key,
|
||||||
|
g => g.OrderByDescending(x => x.UpdatedAt).ThenByDescending(x => x.CreatedAt).First(),
|
||||||
|
StringComparer.Ordinal);
|
||||||
|
|
||||||
|
var userDataByItem = userDatas
|
||||||
|
.Where(x => x.Item > 0)
|
||||||
|
.GroupBy(x => x.Item)
|
||||||
|
.ToDictionary(
|
||||||
|
g => g.Key,
|
||||||
|
g => g.OrderByDescending(x => x.UpdatedAt).ThenByDescending(x => x.CreatedAt).First());
|
||||||
|
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
var updatedLineIds = new HashSet<Guid>();
|
||||||
|
var updatedFields = 0;
|
||||||
|
var appliedIssues = 0;
|
||||||
|
var skippedIssues = 0;
|
||||||
|
|
||||||
|
await using var transaction = await _db.Database.BeginTransactionAsync(cancellationToken);
|
||||||
|
|
||||||
|
foreach (var issue in selectedIssues)
|
||||||
|
{
|
||||||
|
if (!issue.MobileLineId.HasValue || !linesById.TryGetValue(issue.MobileLineId.Value, out var line))
|
||||||
|
{
|
||||||
|
skippedIssues++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!reportSnapshotsByIssueId.TryGetValue(issue.Id, out var reportSnapshot))
|
||||||
|
{
|
||||||
|
skippedIssues++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reportSnapshot == null)
|
||||||
|
{
|
||||||
|
skippedIssues++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var differences = DeserializeDifferences(issue.DifferencesJson);
|
||||||
|
var lineChanged = false;
|
||||||
|
|
||||||
|
var hasLineDifference = differences.Any(x => x.Syncable && x.FieldKey == "line");
|
||||||
|
var hasChipDifference = differences.Any(x => x.Syncable && x.FieldKey == "chip");
|
||||||
|
var hasStatusDifference = differences.Any(x => x.Syncable && x.FieldKey == "status");
|
||||||
|
|
||||||
|
var previousLine = MveAuditNormalization.NullIfEmptyDigits(line.Linha);
|
||||||
|
var nextLine = hasLineDifference ? MveAuditNormalization.NullIfEmptyDigits(reportSnapshot.NumeroLinha) : previousLine;
|
||||||
|
if (hasLineDifference)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(nextLine))
|
||||||
|
{
|
||||||
|
skippedIssues++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var hasConflict = await _db.MobileLines
|
||||||
|
.AsNoTracking()
|
||||||
|
.AnyAsync(
|
||||||
|
x => x.TenantId == line.TenantId &&
|
||||||
|
x.Id != line.Id &&
|
||||||
|
x.Linha == nextLine,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
if (hasConflict)
|
||||||
|
{
|
||||||
|
skippedIssues++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasLineDifference && SetString(line.Linha, nextLine, value => line.Linha = value))
|
||||||
|
{
|
||||||
|
lineChanged = true;
|
||||||
|
updatedFields++;
|
||||||
|
|
||||||
|
SyncLinkedLineRecords(
|
||||||
|
line,
|
||||||
|
previousLine,
|
||||||
|
nextLine,
|
||||||
|
vigenciaByLine,
|
||||||
|
vigenciaByItem,
|
||||||
|
userDataByLine,
|
||||||
|
userDataByItem,
|
||||||
|
now);
|
||||||
|
|
||||||
|
AddTrocaNumeroHistory(
|
||||||
|
line,
|
||||||
|
previousLine,
|
||||||
|
nextLine,
|
||||||
|
MveAuditNormalization.NullIfEmptyDigits(reportSnapshot.Chip) ?? MveAuditNormalization.NullIfEmptyDigits(line.Chip),
|
||||||
|
now);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasChipDifference)
|
||||||
|
{
|
||||||
|
var nextChip = MveAuditNormalization.NullIfEmptyDigits(reportSnapshot.Chip);
|
||||||
|
if (SetString(line.Chip, nextChip, value => line.Chip = value))
|
||||||
|
{
|
||||||
|
lineChanged = true;
|
||||||
|
updatedFields++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasStatusDifference)
|
||||||
|
{
|
||||||
|
var systemStatus = MveAuditNormalization.NormalizeStatusForSystem(reportSnapshot.StatusLinha);
|
||||||
|
if (SetString(line.Status, systemStatus, value => line.Status = value))
|
||||||
|
{
|
||||||
|
ApplyBlockedLineContext(line);
|
||||||
|
lineChanged = true;
|
||||||
|
updatedFields++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lineChanged)
|
||||||
|
{
|
||||||
|
line.UpdatedAt = now;
|
||||||
|
updatedLineIds.Add(line.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
issue.Applied = true;
|
||||||
|
issue.AppliedAtUtc = now;
|
||||||
|
appliedIssues++;
|
||||||
|
}
|
||||||
|
|
||||||
|
run.AppliedIssuesCount = run.Issues.Count(x => x.Applied);
|
||||||
|
run.AppliedLinesCount += updatedLineIds.Count;
|
||||||
|
run.AppliedFieldsCount += updatedFields;
|
||||||
|
run.AppliedAtUtc = now;
|
||||||
|
run.AppliedByUserId = ResolveUserId(_httpContextAccessor.HttpContext?.User);
|
||||||
|
run.AppliedByUserName = ResolveUserName(_httpContextAccessor.HttpContext?.User);
|
||||||
|
run.AppliedByUserEmail = ResolveUserEmail(_httpContextAccessor.HttpContext?.User);
|
||||||
|
run.Status = run.AppliedIssuesCount >= run.TotalSyncableIssues
|
||||||
|
? "APPLIED"
|
||||||
|
: run.AppliedIssuesCount > 0
|
||||||
|
? "PARTIAL_APPLIED"
|
||||||
|
: "READY";
|
||||||
|
|
||||||
|
_db.AuditLogs.Add(BuildAuditLog(
|
||||||
|
action: "MVE_AUDIT_APPLY",
|
||||||
|
runId: run.Id,
|
||||||
|
fileName: run.FileName,
|
||||||
|
changes: new List<AuditFieldChangeDto>
|
||||||
|
{
|
||||||
|
new() { Field = "IssuesAplicadas", ChangeType = "modified", NewValue = appliedIssues.ToString() },
|
||||||
|
new() { Field = "LinhasAtualizadas", ChangeType = "modified", NewValue = updatedLineIds.Count.ToString() },
|
||||||
|
new() { Field = "CamposAtualizados", ChangeType = "modified", NewValue = updatedFields.ToString() }
|
||||||
|
},
|
||||||
|
metadata: new
|
||||||
|
{
|
||||||
|
requestedIssues = result.RequestedIssues,
|
||||||
|
appliedIssues,
|
||||||
|
updatedLines = updatedLineIds.Count,
|
||||||
|
updatedFields,
|
||||||
|
skippedIssues
|
||||||
|
}));
|
||||||
|
|
||||||
|
await _db.SaveChangesAsync(cancellationToken);
|
||||||
|
await transaction.CommitAsync(cancellationToken);
|
||||||
|
|
||||||
|
if (appliedIssues > 0)
|
||||||
|
{
|
||||||
|
await _vigenciaNotificationSyncService.SyncCurrentTenantAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
result.AppliedIssues = appliedIssues;
|
||||||
|
result.UpdatedLines = updatedLineIds.Count;
|
||||||
|
result.UpdatedFields = updatedFields;
|
||||||
|
result.SkippedIssues = skippedIssues;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ValidateInputFile(IFormFile file)
|
||||||
|
{
|
||||||
|
if (file == null || file.Length <= 0)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Selecione um arquivo CSV do MVE para continuar.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file.Length > 20_000_000)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("O arquivo do MVE excede o limite de 20 MB.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var extension = Path.GetExtension(file.FileName ?? string.Empty);
|
||||||
|
if (!string.Equals(extension, ".csv", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("O relatório MVE deve ser enviado em formato CSV.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private MveAuditRunDto ToDto(MveAuditRun run)
|
||||||
|
{
|
||||||
|
return new MveAuditRunDto
|
||||||
|
{
|
||||||
|
Id = run.Id,
|
||||||
|
FileName = run.FileName,
|
||||||
|
FileEncoding = run.FileEncoding,
|
||||||
|
Status = run.Status,
|
||||||
|
ImportedAtUtc = run.ImportedAtUtc,
|
||||||
|
AppliedAtUtc = run.AppliedAtUtc,
|
||||||
|
AppliedByUserName = run.AppliedByUserName,
|
||||||
|
AppliedByUserEmail = run.AppliedByUserEmail,
|
||||||
|
Summary = new MveAuditSummaryDto
|
||||||
|
{
|
||||||
|
TotalSystemLines = run.TotalSystemLines,
|
||||||
|
TotalReportLines = run.TotalReportLines,
|
||||||
|
TotalConciliated = run.TotalConciliated,
|
||||||
|
TotalStatusDivergences = run.TotalStatusDivergences,
|
||||||
|
TotalDataDivergences = run.TotalDataDivergences,
|
||||||
|
TotalOnlyInSystem = run.TotalOnlyInSystem,
|
||||||
|
TotalOnlyInReport = run.TotalOnlyInReport,
|
||||||
|
TotalDuplicateReportLines = run.TotalDuplicateReportLines,
|
||||||
|
TotalDuplicateSystemLines = run.TotalDuplicateSystemLines,
|
||||||
|
TotalInvalidRows = run.TotalInvalidRows,
|
||||||
|
TotalUnknownStatuses = run.TotalUnknownStatuses,
|
||||||
|
TotalSyncableIssues = run.TotalSyncableIssues,
|
||||||
|
AppliedIssuesCount = run.AppliedIssuesCount,
|
||||||
|
AppliedLinesCount = run.AppliedLinesCount,
|
||||||
|
AppliedFieldsCount = run.AppliedFieldsCount
|
||||||
|
},
|
||||||
|
Issues = run.Issues
|
||||||
|
.OrderByDescending(x => x.Syncable)
|
||||||
|
.ThenByDescending(x => x.Severity)
|
||||||
|
.ThenBy(x => x.NumeroLinha)
|
||||||
|
.Select(issue => new MveAuditIssueDto
|
||||||
|
{
|
||||||
|
Id = issue.Id,
|
||||||
|
SourceRowNumber = issue.SourceRowNumber,
|
||||||
|
NumeroLinha = issue.NumeroLinha,
|
||||||
|
MobileLineId = issue.MobileLineId,
|
||||||
|
SystemItem = issue.SystemItem,
|
||||||
|
IssueType = issue.IssueType,
|
||||||
|
Situation = issue.Situation,
|
||||||
|
Severity = issue.Severity,
|
||||||
|
Syncable = issue.Syncable,
|
||||||
|
Applied = issue.Applied,
|
||||||
|
ActionSuggestion = issue.ActionSuggestion,
|
||||||
|
Notes = issue.Notes,
|
||||||
|
SystemStatus = issue.SystemStatus,
|
||||||
|
ReportStatus = issue.ReportStatus,
|
||||||
|
SystemPlan = issue.SystemPlan,
|
||||||
|
ReportPlan = issue.ReportPlan,
|
||||||
|
SystemSnapshot = DeserializeSnapshot(issue.SystemSnapshotJson),
|
||||||
|
ReportSnapshot = DeserializeSnapshot(issue.ReportSnapshotJson),
|
||||||
|
Differences = DeserializeDifferences(issue.DifferencesJson)
|
||||||
|
})
|
||||||
|
.ToList()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private AuditLog BuildAuditLog(
|
||||||
|
string action,
|
||||||
|
Guid runId,
|
||||||
|
string? fileName,
|
||||||
|
IReadOnlyCollection<AuditFieldChangeDto> changes,
|
||||||
|
object metadata)
|
||||||
|
{
|
||||||
|
var actorTenantId = _tenantProvider.ActorTenantId;
|
||||||
|
if (!actorTenantId.HasValue || actorTenantId.Value == Guid.Empty)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Tenant inválido para registrar auditoria MVE.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var user = _httpContextAccessor.HttpContext?.User;
|
||||||
|
var request = _httpContextAccessor.HttpContext?.Request;
|
||||||
|
|
||||||
|
return new AuditLog
|
||||||
|
{
|
||||||
|
TenantId = actorTenantId.Value,
|
||||||
|
ActorTenantId = actorTenantId.Value,
|
||||||
|
TargetTenantId = actorTenantId.Value,
|
||||||
|
ActorUserId = ResolveUserId(user),
|
||||||
|
UserId = ResolveUserId(user),
|
||||||
|
UserName = ResolveUserName(user),
|
||||||
|
UserEmail = ResolveUserEmail(user),
|
||||||
|
OccurredAtUtc = DateTime.UtcNow,
|
||||||
|
Action = action,
|
||||||
|
Page = "Geral",
|
||||||
|
EntityName = "MveAudit",
|
||||||
|
EntityId = runId.ToString(),
|
||||||
|
EntityLabel = string.IsNullOrWhiteSpace(fileName) ? "Auditoria MVE" : fileName.Trim(),
|
||||||
|
ChangesJson = JsonSerializer.Serialize(changes, JsonOptions),
|
||||||
|
MetadataJson = JsonSerializer.Serialize(metadata, JsonOptions),
|
||||||
|
RequestPath = request?.Path.Value,
|
||||||
|
RequestMethod = request?.Method,
|
||||||
|
IpAddress = _httpContextAccessor.HttpContext?.Connection?.RemoteIpAddress?.ToString()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MveAuditSnapshotDto? DeserializeSnapshot(string? json)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(json))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return JsonSerializer.Deserialize<MveAuditSnapshotDto>(json, JsonOptions);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<MveAuditDifferenceDto> DeserializeDifferences(string? json)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(json))
|
||||||
|
{
|
||||||
|
return new List<MveAuditDifferenceDto>();
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return JsonSerializer.Deserialize<List<MveAuditDifferenceDto>>(json, JsonOptions) ?? new List<MveAuditDifferenceDto>();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return new List<MveAuditDifferenceDto>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SyncLinkedLineRecords(
|
||||||
|
MobileLine line,
|
||||||
|
string? previousLine,
|
||||||
|
string? nextLine,
|
||||||
|
IDictionary<string, VigenciaLine> vigenciaByLine,
|
||||||
|
IDictionary<int, VigenciaLine> vigenciaByItem,
|
||||||
|
IDictionary<string, UserData> userDataByLine,
|
||||||
|
IDictionary<int, UserData> userDataByItem,
|
||||||
|
DateTime now)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(nextLine))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var lookupLine = !string.IsNullOrWhiteSpace(previousLine) ? previousLine : nextLine;
|
||||||
|
|
||||||
|
var vigencia = ResolveVigencia(line, lookupLine, vigenciaByLine, vigenciaByItem);
|
||||||
|
if (vigencia != null && SetString(vigencia.Linha, nextLine, value => vigencia.Linha = value))
|
||||||
|
{
|
||||||
|
vigencia.UpdatedAt = now;
|
||||||
|
RefreshLineLookup(vigenciaByLine, previousLine, nextLine, vigencia);
|
||||||
|
}
|
||||||
|
|
||||||
|
var userData = ResolveUserData(line, lookupLine, userDataByLine, userDataByItem);
|
||||||
|
if (userData != null && SetString(userData.Linha, nextLine, value => userData.Linha = value))
|
||||||
|
{
|
||||||
|
userData.UpdatedAt = now;
|
||||||
|
RefreshLineLookup(userDataByLine, previousLine, nextLine, userData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddTrocaNumeroHistory(
|
||||||
|
MobileLine line,
|
||||||
|
string? previousLine,
|
||||||
|
string? nextLine,
|
||||||
|
string? chip,
|
||||||
|
DateTime now)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(previousLine) ||
|
||||||
|
string.IsNullOrWhiteSpace(nextLine) ||
|
||||||
|
string.Equals(previousLine, nextLine, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_db.TrocaNumeroLines.Add(new TrocaNumeroLine
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
TenantId = line.TenantId,
|
||||||
|
Item = line.Item,
|
||||||
|
LinhaAntiga = previousLine,
|
||||||
|
LinhaNova = nextLine,
|
||||||
|
ICCID = MveAuditNormalization.NullIfEmptyDigits(chip),
|
||||||
|
DataTroca = now,
|
||||||
|
Motivo = "Auditoria MVE",
|
||||||
|
Observacao = "Linha atualizada automaticamente a partir do relatório MVE.",
|
||||||
|
CreatedAt = now,
|
||||||
|
UpdatedAt = now
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void RefreshLineLookup<T>(
|
||||||
|
IDictionary<string, T> lookup,
|
||||||
|
string? previousLine,
|
||||||
|
string? nextLine,
|
||||||
|
T entity)
|
||||||
|
where T : class
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(previousLine))
|
||||||
|
{
|
||||||
|
lookup.Remove(previousLine);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(nextLine))
|
||||||
|
{
|
||||||
|
lookup[nextLine] = entity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private VigenciaLine? ResolveVigencia(
|
||||||
|
MobileLine line,
|
||||||
|
string? numeroLinha,
|
||||||
|
IDictionary<string, VigenciaLine> vigenciaByLine,
|
||||||
|
IDictionary<int, VigenciaLine> vigenciaByItem)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(numeroLinha) && vigenciaByLine.TryGetValue(numeroLinha, out var byLine))
|
||||||
|
{
|
||||||
|
return byLine;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line.Item > 0 && vigenciaByItem.TryGetValue(line.Item, out var byItem))
|
||||||
|
{
|
||||||
|
return byItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private UserData? ResolveUserData(
|
||||||
|
MobileLine line,
|
||||||
|
string? numeroLinha,
|
||||||
|
IDictionary<string, UserData> userDataByLine,
|
||||||
|
IDictionary<int, UserData> userDataByItem)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(numeroLinha) && userDataByLine.TryGetValue(numeroLinha, out var byLine))
|
||||||
|
{
|
||||||
|
return byLine;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line.Item > 0 && userDataByItem.TryGetValue(line.Item, out var byItem))
|
||||||
|
{
|
||||||
|
return byItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private VigenciaLine CreateVigencia(MobileLine line)
|
||||||
|
{
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
var vigencia = new VigenciaLine
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
TenantId = line.TenantId,
|
||||||
|
Item = line.Item,
|
||||||
|
Linha = MveAuditNormalization.NullIfEmptyDigits(line.Linha),
|
||||||
|
Conta = line.Conta,
|
||||||
|
Cliente = line.Cliente,
|
||||||
|
Usuario = line.Usuario,
|
||||||
|
PlanoContrato = line.PlanoContrato,
|
||||||
|
CreatedAt = now,
|
||||||
|
UpdatedAt = now
|
||||||
|
};
|
||||||
|
|
||||||
|
_db.VigenciaLines.Add(vigencia);
|
||||||
|
return vigencia;
|
||||||
|
}
|
||||||
|
|
||||||
|
private UserData CreateUserData(MobileLine line)
|
||||||
|
{
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
var userData = new UserData
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
TenantId = line.TenantId,
|
||||||
|
Item = line.Item,
|
||||||
|
Linha = MveAuditNormalization.NullIfEmptyDigits(line.Linha),
|
||||||
|
Cliente = line.Cliente,
|
||||||
|
CreatedAt = now,
|
||||||
|
UpdatedAt = now
|
||||||
|
};
|
||||||
|
|
||||||
|
_db.UserDatas.Add(userData);
|
||||||
|
return userData;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Aparelho EnsureAparelho(MobileLine line)
|
||||||
|
{
|
||||||
|
if (line.Aparelho != null)
|
||||||
|
{
|
||||||
|
return line.Aparelho;
|
||||||
|
}
|
||||||
|
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
var aparelho = new Aparelho
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
TenantId = line.TenantId,
|
||||||
|
CreatedAt = now,
|
||||||
|
UpdatedAt = now
|
||||||
|
};
|
||||||
|
|
||||||
|
_db.Aparelhos.Add(aparelho);
|
||||||
|
line.AparelhoId = aparelho.Id;
|
||||||
|
line.Aparelho = aparelho;
|
||||||
|
return aparelho;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ApplyBlockedLineContext(MobileLine line)
|
||||||
|
{
|
||||||
|
var normalized = MveAuditNormalization.NormalizeSystemStatus(line.Status).Key;
|
||||||
|
if (normalized is not "BLOQUEIO_PERDA_ROUBO" and not "BLOQUEIO_120_DIAS")
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
line.Usuario = "RESERVA";
|
||||||
|
line.Skil = "RESERVA";
|
||||||
|
if (string.IsNullOrWhiteSpace(line.Cliente))
|
||||||
|
{
|
||||||
|
line.Cliente = "RESERVA";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool SetString(string? currentValue, string? nextValue, Action<string?> assign)
|
||||||
|
{
|
||||||
|
var normalizedNext = string.IsNullOrWhiteSpace(nextValue)
|
||||||
|
? null
|
||||||
|
: MveAuditNormalization.CleanTextValue(nextValue);
|
||||||
|
|
||||||
|
var normalizedCurrent = string.IsNullOrWhiteSpace(currentValue)
|
||||||
|
? null
|
||||||
|
: MveAuditNormalization.CleanTextValue(currentValue);
|
||||||
|
|
||||||
|
if (string.Equals(normalizedCurrent, normalizedNext, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
assign(normalizedNext);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool SetDate(DateTime? currentValue, DateTime? nextValue, Action<DateTime?> assign)
|
||||||
|
{
|
||||||
|
var normalizedCurrent = currentValue?.Date;
|
||||||
|
var normalizedNext = nextValue?.Date;
|
||||||
|
if (normalizedCurrent == normalizedNext)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
assign(nextValue);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
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,330 @@
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
||||||
|
namespace line_gestao_api.Services;
|
||||||
|
|
||||||
|
public sealed class MveCsvParserService
|
||||||
|
{
|
||||||
|
private static readonly Encoding StrictUtf8 = new UTF8Encoding(false, true);
|
||||||
|
private static readonly Encoding Latin1 = Encoding.GetEncoding("ISO-8859-1");
|
||||||
|
private static readonly Encoding Windows1252 = Encoding.GetEncoding(1252);
|
||||||
|
private static readonly string[] RequiredHeaders = ["DDD", "NUMERO", "STATUS_LINHA"];
|
||||||
|
|
||||||
|
static MveCsvParserService()
|
||||||
|
{
|
||||||
|
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal async Task<MveParsedFileResult> ParseAsync(IFormFile file, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(file);
|
||||||
|
|
||||||
|
await using var stream = file.OpenReadStream();
|
||||||
|
using var memory = new MemoryStream();
|
||||||
|
await stream.CopyToAsync(memory, cancellationToken);
|
||||||
|
return Parse(memory.ToArray(), file.FileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal MveParsedFileResult Parse(byte[] bytes, string? fileName)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(bytes);
|
||||||
|
|
||||||
|
var decoded = Decode(bytes);
|
||||||
|
var rows = ParseCsvRows(decoded.Content);
|
||||||
|
if (rows.Count == 0)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("O arquivo CSV do MVE está vazio.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var headerRow = rows[0];
|
||||||
|
var headerMap = BuildHeaderMap(headerRow);
|
||||||
|
var missingHeaders = RequiredHeaders
|
||||||
|
.Where(header => !headerMap.ContainsKey(MveAuditNormalization.NormalizeHeader(header)))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (missingHeaders.Count > 0)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"O relatório MVE não contém as colunas obrigatórias: {string.Join(", ", missingHeaders)}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var serviceColumns = headerMap
|
||||||
|
.Where(entry => entry.Key.StartsWith("SERVICO_ATIVOS", StringComparison.Ordinal))
|
||||||
|
.OrderBy(entry => entry.Value)
|
||||||
|
.Select(entry => entry.Value)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var result = new MveParsedFileResult
|
||||||
|
{
|
||||||
|
FileName = string.IsNullOrWhiteSpace(fileName) ? null : fileName.Trim(),
|
||||||
|
FileEncoding = decoded.Encoding.WebName,
|
||||||
|
FileHashSha256 = Convert.ToHexString(SHA256.HashData(bytes)).ToLowerInvariant(),
|
||||||
|
SourceRowCount = Math.Max(rows.Count - 1, 0)
|
||||||
|
};
|
||||||
|
|
||||||
|
for (var rowIndex = 1; rowIndex < rows.Count; rowIndex++)
|
||||||
|
{
|
||||||
|
var row = rows[rowIndex];
|
||||||
|
if (IsEmptyRow(row))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var sourceRowNumber = rowIndex + 1;
|
||||||
|
var ddd = GetValue(row, headerMap, "DDD");
|
||||||
|
var numero = GetValue(row, headerMap, "NUMERO");
|
||||||
|
var numeroNormalizado = MveAuditNormalization.OnlyDigits($"{ddd}{numero}");
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(ddd) || string.IsNullOrWhiteSpace(numero) || string.IsNullOrWhiteSpace(numeroNormalizado))
|
||||||
|
{
|
||||||
|
result.Issues.Add(new MveParsedIssue(
|
||||||
|
sourceRowNumber,
|
||||||
|
string.Empty,
|
||||||
|
"INVALID_ROW",
|
||||||
|
"Linha sem DDD e/ou número válido no relatório MVE."));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var status = MveAuditNormalization.NormalizeReportStatus(GetValue(row, headerMap, "STATUS_LINHA"));
|
||||||
|
var line = new MveParsedLine
|
||||||
|
{
|
||||||
|
SourceRowNumber = sourceRowNumber,
|
||||||
|
Ddd = ddd,
|
||||||
|
Numero = numero,
|
||||||
|
NumeroNormalizado = numeroNormalizado,
|
||||||
|
StatusLinha = status.DisplayValue,
|
||||||
|
StatusLinhaKey = status.Key,
|
||||||
|
StatusLinhaRecognized = status.Recognized,
|
||||||
|
StatusConta = GetValue(row, headerMap, "STATUS_CONTA"),
|
||||||
|
PlanoLinha = GetValue(row, headerMap, "PLANO_LINHA"),
|
||||||
|
DataAtivacao = MveAuditNormalization.ParseDateValue(GetValue(row, headerMap, "DATA_ATIVACAO")),
|
||||||
|
TerminoContrato = MveAuditNormalization.ParseDateValue(GetValue(row, headerMap, "TERMINO_CONTRATO")),
|
||||||
|
Chip = MveAuditNormalization.NullIfEmptyDigits(GetValue(row, headerMap, "CHIP")),
|
||||||
|
Conta = NullIfEmpty(MveAuditNormalization.CleanTextValue(GetValue(row, headerMap, "CONTA"))),
|
||||||
|
Cnpj = MveAuditNormalization.NullIfEmptyDigits(GetValue(row, headerMap, "CNPJ")),
|
||||||
|
ModeloAparelho = NullIfEmpty(MveAuditNormalization.CleanTextValue(GetValue(row, headerMap, "MODELO_APARELHO"))),
|
||||||
|
Fabricante = NullIfEmpty(MveAuditNormalization.CleanTextValue(GetValue(row, headerMap, "FABRICANTE")))
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var columnIndex in serviceColumns)
|
||||||
|
{
|
||||||
|
if (columnIndex < 0 || columnIndex >= row.Count)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var serviceValue = NullIfEmpty(MveAuditNormalization.CleanTextValue(row[columnIndex]));
|
||||||
|
if (!string.IsNullOrWhiteSpace(serviceValue))
|
||||||
|
{
|
||||||
|
line.ServicosAtivos.Add(serviceValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Lines.Add(line);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DecodedContent Decode(byte[] bytes)
|
||||||
|
{
|
||||||
|
foreach (var encoding in new[] { StrictUtf8, Windows1252, Latin1 })
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var content = encoding.GetString(bytes);
|
||||||
|
if (!string.IsNullOrWhiteSpace(content))
|
||||||
|
{
|
||||||
|
return new DecodedContent(encoding, content.TrimStart('\uFEFF'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// tenta o próximo encoding
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new DecodedContent(Latin1, Latin1.GetString(bytes).TrimStart('\uFEFF'));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<List<string>> ParseCsvRows(string content)
|
||||||
|
{
|
||||||
|
var rows = new List<List<string>>();
|
||||||
|
var currentRow = new List<string>();
|
||||||
|
var currentField = new StringBuilder();
|
||||||
|
var inQuotes = false;
|
||||||
|
|
||||||
|
for (var i = 0; i < content.Length; i++)
|
||||||
|
{
|
||||||
|
var ch = content[i];
|
||||||
|
|
||||||
|
if (inQuotes)
|
||||||
|
{
|
||||||
|
if (ch == '"')
|
||||||
|
{
|
||||||
|
if (i + 1 < content.Length && content[i + 1] == '"')
|
||||||
|
{
|
||||||
|
currentField.Append('"');
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
inQuotes = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
currentField.Append(ch);
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (ch)
|
||||||
|
{
|
||||||
|
case '"':
|
||||||
|
inQuotes = true;
|
||||||
|
break;
|
||||||
|
case ';':
|
||||||
|
currentRow.Add(currentField.ToString());
|
||||||
|
currentField.Clear();
|
||||||
|
break;
|
||||||
|
case '\r':
|
||||||
|
if (i + 1 < content.Length && content[i + 1] == '\n')
|
||||||
|
{
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentRow.Add(currentField.ToString());
|
||||||
|
currentField.Clear();
|
||||||
|
rows.Add(currentRow);
|
||||||
|
currentRow = new List<string>();
|
||||||
|
break;
|
||||||
|
case '\n':
|
||||||
|
currentRow.Add(currentField.ToString());
|
||||||
|
currentField.Clear();
|
||||||
|
rows.Add(currentRow);
|
||||||
|
currentRow = new List<string>();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
currentField.Append(ch);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
currentRow.Add(currentField.ToString());
|
||||||
|
if (currentRow.Count > 1 || !string.IsNullOrWhiteSpace(currentRow[0]))
|
||||||
|
{
|
||||||
|
rows.Add(currentRow);
|
||||||
|
}
|
||||||
|
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Dictionary<string, int> BuildHeaderMap(IReadOnlyList<string> headerRow)
|
||||||
|
{
|
||||||
|
var map = new Dictionary<string, int>(StringComparer.Ordinal);
|
||||||
|
for (var i = 0; i < headerRow.Count; i++)
|
||||||
|
{
|
||||||
|
var key = MveAuditNormalization.NormalizeHeader(headerRow[i]);
|
||||||
|
if (string.IsNullOrWhiteSpace(key) || key.StartsWith("UNNAMED", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!map.ContainsKey(key))
|
||||||
|
{
|
||||||
|
map[key] = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetValue(IReadOnlyList<string> row, IReadOnlyDictionary<string, int> headerMap, string header)
|
||||||
|
{
|
||||||
|
var normalizedHeader = MveAuditNormalization.NormalizeHeader(header);
|
||||||
|
if (!headerMap.TryGetValue(normalizedHeader, out var index))
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index < 0 || index >= row.Count)
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
return MveAuditNormalization.CleanTextValue(row[index]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsEmptyRow(IReadOnlyList<string> row)
|
||||||
|
{
|
||||||
|
return row.Count == 0 || row.All(cell => string.IsNullOrWhiteSpace(MveAuditNormalization.CleanTextValue(cell)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? NullIfEmpty(string? value)
|
||||||
|
{
|
||||||
|
return string.IsNullOrWhiteSpace(value) ? null : value.Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed record DecodedContent(Encoding Encoding, string Content);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class MveParsedFileResult
|
||||||
|
{
|
||||||
|
public string? FileName { get; init; }
|
||||||
|
|
||||||
|
public string FileEncoding { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
public string FileHashSha256 { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
public int SourceRowCount { get; init; }
|
||||||
|
|
||||||
|
public List<MveParsedLine> Lines { get; } = new();
|
||||||
|
|
||||||
|
public List<MveParsedIssue> Issues { get; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class MveParsedLine
|
||||||
|
{
|
||||||
|
public int SourceRowNumber { get; init; }
|
||||||
|
|
||||||
|
public string Ddd { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
public string Numero { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
public string NumeroNormalizado { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
public string StatusLinha { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
public string StatusLinhaKey { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
public bool StatusLinhaRecognized { get; init; }
|
||||||
|
|
||||||
|
public string? StatusConta { get; init; }
|
||||||
|
|
||||||
|
public string? PlanoLinha { get; init; }
|
||||||
|
|
||||||
|
public DateTime? DataAtivacao { get; init; }
|
||||||
|
|
||||||
|
public DateTime? TerminoContrato { get; init; }
|
||||||
|
|
||||||
|
public string? Chip { get; init; }
|
||||||
|
|
||||||
|
public string? Conta { get; init; }
|
||||||
|
|
||||||
|
public string? Cnpj { get; init; }
|
||||||
|
|
||||||
|
public string? ModeloAparelho { get; init; }
|
||||||
|
|
||||||
|
public string? Fabricante { get; init; }
|
||||||
|
|
||||||
|
public List<string> ServicosAtivos { get; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed record MveParsedIssue(
|
||||||
|
int SourceRowNumber,
|
||||||
|
string NumeroLinha,
|
||||||
|
string IssueType,
|
||||||
|
string Message);
|
||||||
|
|
@ -0,0 +1,743 @@
|
||||||
|
using line_gestao_api.Data;
|
||||||
|
using line_gestao_api.Dtos;
|
||||||
|
using line_gestao_api.Models;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace line_gestao_api.Services;
|
||||||
|
|
||||||
|
public sealed class MveReconciliationService
|
||||||
|
{
|
||||||
|
private readonly AppDbContext _db;
|
||||||
|
|
||||||
|
public MveReconciliationService(AppDbContext db)
|
||||||
|
{
|
||||||
|
_db = db;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal async Task<MveReconciliationResult> BuildAsync(
|
||||||
|
MveParsedFileResult parsedFile,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(parsedFile);
|
||||||
|
|
||||||
|
var mobileLines = await _db.MobileLines
|
||||||
|
.AsNoTracking()
|
||||||
|
.Include(x => x.Aparelho)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
var vigencias = await _db.VigenciaLines
|
||||||
|
.AsNoTracking()
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
var userDatas = await _db.UserDatas
|
||||||
|
.AsNoTracking()
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
var systemAggregates = BuildSystemAggregates(mobileLines, vigencias, userDatas);
|
||||||
|
var systemByNumber = systemAggregates
|
||||||
|
.Where(x => !string.IsNullOrWhiteSpace(x.NumeroNormalizado))
|
||||||
|
.GroupBy(x => x.NumeroNormalizado, StringComparer.Ordinal)
|
||||||
|
.ToDictionary(g => g.Key, g => g.ToList(), StringComparer.Ordinal);
|
||||||
|
|
||||||
|
var reportByNumber = parsedFile.Lines
|
||||||
|
.GroupBy(x => x.NumeroNormalizado, StringComparer.Ordinal)
|
||||||
|
.ToDictionary(g => g.Key, g => g.ToList(), StringComparer.Ordinal);
|
||||||
|
|
||||||
|
var result = new MveReconciliationResult
|
||||||
|
{
|
||||||
|
TotalSystemLines = mobileLines.Count,
|
||||||
|
TotalReportLines = parsedFile.Lines.Count,
|
||||||
|
TotalInvalidRows = parsedFile.Issues.Count(x => x.IssueType == "INVALID_ROW"),
|
||||||
|
TotalUnknownStatuses = parsedFile.Lines.Count(x => !x.StatusLinhaRecognized)
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var parserIssue in parsedFile.Issues)
|
||||||
|
{
|
||||||
|
result.Issues.Add(new MveReconciliationIssueResult
|
||||||
|
{
|
||||||
|
SourceRowNumber = parserIssue.SourceRowNumber,
|
||||||
|
NumeroLinha = parserIssue.NumeroLinha,
|
||||||
|
IssueType = parserIssue.IssueType,
|
||||||
|
Situation = "linha inválida no relatório",
|
||||||
|
Severity = "WARNING",
|
||||||
|
Syncable = false,
|
||||||
|
ActionSuggestion = "Corrigir o arquivo MVE e refazer a auditoria",
|
||||||
|
Notes = parserIssue.Message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var duplicateReportKeys = reportByNumber
|
||||||
|
.Where(entry => entry.Value.Count > 1)
|
||||||
|
.Select(entry => entry.Key)
|
||||||
|
.ToHashSet(StringComparer.Ordinal);
|
||||||
|
|
||||||
|
foreach (var duplicateKey in duplicateReportKeys)
|
||||||
|
{
|
||||||
|
var duplicates = reportByNumber[duplicateKey];
|
||||||
|
var first = duplicates[0];
|
||||||
|
result.TotalDuplicateReportLines++;
|
||||||
|
result.Issues.Add(new MveReconciliationIssueResult
|
||||||
|
{
|
||||||
|
SourceRowNumber = first.SourceRowNumber,
|
||||||
|
NumeroLinha = duplicateKey,
|
||||||
|
IssueType = "DUPLICATE_REPORT",
|
||||||
|
Situation = "duplicidade no relatório",
|
||||||
|
Severity = "WARNING",
|
||||||
|
Syncable = false,
|
||||||
|
ActionSuggestion = "Corrigir a duplicidade no relatório MVE",
|
||||||
|
Notes = $"A linha {duplicateKey} apareceu {duplicates.Count} vezes no arquivo MVE.",
|
||||||
|
ReportStatus = first.StatusLinha,
|
||||||
|
ReportPlan = first.PlanoLinha,
|
||||||
|
ReportSnapshot = BuildReportSnapshot(first)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var duplicateSystemKeys = systemByNumber
|
||||||
|
.Where(entry => entry.Value.Count > 1)
|
||||||
|
.Select(entry => entry.Key)
|
||||||
|
.ToHashSet(StringComparer.Ordinal);
|
||||||
|
|
||||||
|
foreach (var duplicateKey in duplicateSystemKeys)
|
||||||
|
{
|
||||||
|
var duplicates = systemByNumber[duplicateKey];
|
||||||
|
var first = duplicates[0];
|
||||||
|
result.TotalDuplicateSystemLines++;
|
||||||
|
result.Issues.Add(new MveReconciliationIssueResult
|
||||||
|
{
|
||||||
|
NumeroLinha = duplicateKey,
|
||||||
|
MobileLineId = first.MobileLine.Id,
|
||||||
|
SystemItem = first.MobileLine.Item,
|
||||||
|
IssueType = "DUPLICATE_SYSTEM",
|
||||||
|
Situation = "duplicidade no sistema",
|
||||||
|
Severity = "WARNING",
|
||||||
|
Syncable = false,
|
||||||
|
ActionSuggestion = "Corrigir a duplicidade interna antes de sincronizar",
|
||||||
|
Notes = $"A linha {duplicateKey} possui {duplicates.Count} registros no sistema.",
|
||||||
|
SystemStatus = first.MobileLine.Status,
|
||||||
|
SystemPlan = first.MobileLine.PlanoContrato,
|
||||||
|
SystemSnapshot = BuildSystemSnapshot(first)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var blockedSystemKeys = new HashSet<string>(duplicateSystemKeys, StringComparer.Ordinal);
|
||||||
|
var blockedReportKeys = new HashSet<string>(duplicateReportKeys, StringComparer.Ordinal);
|
||||||
|
var matchedSystemLineIds = new HashSet<Guid>();
|
||||||
|
var matchedReportRows = new HashSet<int>();
|
||||||
|
|
||||||
|
var allKeys = reportByNumber.Keys
|
||||||
|
.Concat(systemByNumber.Keys)
|
||||||
|
.Where(key => !string.IsNullOrWhiteSpace(key))
|
||||||
|
.Distinct(StringComparer.Ordinal)
|
||||||
|
.OrderBy(key => key, StringComparer.Ordinal)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
foreach (var key in allKeys)
|
||||||
|
{
|
||||||
|
if (blockedSystemKeys.Contains(key) || blockedReportKeys.Contains(key))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var hasReport = reportByNumber.TryGetValue(key, out var reportLines);
|
||||||
|
var hasSystem = systemByNumber.TryGetValue(key, out var systemLines);
|
||||||
|
var reportLine = hasReport ? reportLines![0] : null;
|
||||||
|
var systemLine = hasSystem ? systemLines![0] : null;
|
||||||
|
|
||||||
|
if (reportLine == null || systemLine == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
matchedSystemLineIds.Add(systemLine.MobileLine.Id);
|
||||||
|
matchedReportRows.Add(reportLine.SourceRowNumber);
|
||||||
|
|
||||||
|
var comparison = CompareMatchedLine(systemLine, reportLine);
|
||||||
|
RegisterComparisonResult(result, comparison);
|
||||||
|
}
|
||||||
|
|
||||||
|
var unmatchedSystemLines = systemAggregates
|
||||||
|
.Where(x => !blockedSystemKeys.Contains(x.NumeroNormalizado))
|
||||||
|
.Where(x => !matchedSystemLineIds.Contains(x.MobileLine.Id))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var unmatchedReportLines = parsedFile.Lines
|
||||||
|
.Where(x => !blockedReportKeys.Contains(x.NumeroNormalizado))
|
||||||
|
.Where(x => !matchedReportRows.Contains(x.SourceRowNumber))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var chipMatchedSystemLineIds = new HashSet<Guid>();
|
||||||
|
var chipMatchedReportRows = new HashSet<int>();
|
||||||
|
|
||||||
|
var systemByChip = unmatchedSystemLines
|
||||||
|
.Where(x => !string.IsNullOrWhiteSpace(MveAuditNormalization.NullIfEmptyDigits(x.MobileLine.Chip)))
|
||||||
|
.GroupBy(x => MveAuditNormalization.OnlyDigits(x.MobileLine.Chip), StringComparer.Ordinal)
|
||||||
|
.ToDictionary(g => g.Key, g => g.ToList(), StringComparer.Ordinal);
|
||||||
|
|
||||||
|
var reportByChip = unmatchedReportLines
|
||||||
|
.Where(x => !string.IsNullOrWhiteSpace(MveAuditNormalization.NullIfEmptyDigits(x.Chip)))
|
||||||
|
.GroupBy(x => MveAuditNormalization.OnlyDigits(x.Chip), StringComparer.Ordinal)
|
||||||
|
.ToDictionary(g => g.Key, g => g.ToList(), StringComparer.Ordinal);
|
||||||
|
|
||||||
|
var chipKeys = systemByChip.Keys
|
||||||
|
.Intersect(reportByChip.Keys, StringComparer.Ordinal)
|
||||||
|
.OrderBy(key => key, StringComparer.Ordinal)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
foreach (var chipKey in chipKeys)
|
||||||
|
{
|
||||||
|
var systemCandidates = systemByChip[chipKey];
|
||||||
|
var reportCandidates = reportByChip[chipKey];
|
||||||
|
if (systemCandidates.Count != 1 || reportCandidates.Count != 1)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var systemLine = systemCandidates[0];
|
||||||
|
var reportLine = reportCandidates[0];
|
||||||
|
|
||||||
|
chipMatchedSystemLineIds.Add(systemLine.MobileLine.Id);
|
||||||
|
chipMatchedReportRows.Add(reportLine.SourceRowNumber);
|
||||||
|
|
||||||
|
var comparison = CompareMatchedByChip(systemLine, reportLine);
|
||||||
|
RegisterComparisonResult(result, comparison);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var systemLine in unmatchedSystemLines.Where(x => !chipMatchedSystemLineIds.Contains(x.MobileLine.Id)))
|
||||||
|
{
|
||||||
|
result.TotalOnlyInSystem++;
|
||||||
|
result.Issues.Add(new MveReconciliationIssueResult
|
||||||
|
{
|
||||||
|
NumeroLinha = systemLine.NumeroNormalizado,
|
||||||
|
MobileLineId = systemLine.MobileLine.Id,
|
||||||
|
SystemItem = systemLine.MobileLine.Item,
|
||||||
|
IssueType = "ONLY_IN_SYSTEM",
|
||||||
|
Situation = "ausente no relatório",
|
||||||
|
Severity = "WARNING",
|
||||||
|
Syncable = false,
|
||||||
|
ActionSuggestion = "Validar com a Vivo antes de alterar o cadastro",
|
||||||
|
Notes = "A linha existe no sistema, mas não foi encontrada no relatório MVE.",
|
||||||
|
SystemStatus = systemLine.MobileLine.Status,
|
||||||
|
SystemPlan = systemLine.MobileLine.PlanoContrato,
|
||||||
|
SystemSnapshot = BuildSystemSnapshot(systemLine)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var reportLine in unmatchedReportLines.Where(x => !chipMatchedReportRows.Contains(x.SourceRowNumber)))
|
||||||
|
{
|
||||||
|
result.TotalOnlyInReport++;
|
||||||
|
result.Issues.Add(new MveReconciliationIssueResult
|
||||||
|
{
|
||||||
|
SourceRowNumber = reportLine.SourceRowNumber,
|
||||||
|
NumeroLinha = reportLine.NumeroNormalizado,
|
||||||
|
IssueType = "ONLY_IN_REPORT",
|
||||||
|
Situation = "ausente no sistema",
|
||||||
|
Severity = "WARNING",
|
||||||
|
Syncable = false,
|
||||||
|
ActionSuggestion = "Avaliar cadastro manual dessa linha",
|
||||||
|
Notes = "A linha existe no relatório MVE, mas não foi encontrada na página Geral.",
|
||||||
|
ReportStatus = reportLine.StatusLinha,
|
||||||
|
ReportPlan = reportLine.PlanoLinha,
|
||||||
|
ReportSnapshot = BuildReportSnapshot(reportLine)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<MveSystemLineAggregate> BuildSystemAggregates(
|
||||||
|
IReadOnlyCollection<MobileLine> mobileLines,
|
||||||
|
IReadOnlyCollection<VigenciaLine> vigencias,
|
||||||
|
IReadOnlyCollection<UserData> userDatas)
|
||||||
|
{
|
||||||
|
var vigenciaByLine = vigencias
|
||||||
|
.Where(x => !string.IsNullOrWhiteSpace(x.Linha))
|
||||||
|
.GroupBy(x => MveAuditNormalization.OnlyDigits(x.Linha), StringComparer.Ordinal)
|
||||||
|
.ToDictionary(
|
||||||
|
g => g.Key,
|
||||||
|
g => g.OrderByDescending(x => x.UpdatedAt).ThenByDescending(x => x.CreatedAt).First(),
|
||||||
|
StringComparer.Ordinal);
|
||||||
|
|
||||||
|
var vigenciaByItem = vigencias
|
||||||
|
.Where(x => x.Item > 0)
|
||||||
|
.GroupBy(x => x.Item)
|
||||||
|
.ToDictionary(
|
||||||
|
g => g.Key,
|
||||||
|
g => g.OrderByDescending(x => x.UpdatedAt).ThenByDescending(x => x.CreatedAt).First());
|
||||||
|
|
||||||
|
var userDataByLine = userDatas
|
||||||
|
.Where(x => !string.IsNullOrWhiteSpace(x.Linha))
|
||||||
|
.GroupBy(x => MveAuditNormalization.OnlyDigits(x.Linha), StringComparer.Ordinal)
|
||||||
|
.ToDictionary(
|
||||||
|
g => g.Key,
|
||||||
|
g => g.OrderByDescending(x => x.UpdatedAt).ThenByDescending(x => x.CreatedAt).First(),
|
||||||
|
StringComparer.Ordinal);
|
||||||
|
|
||||||
|
var userDataByItem = userDatas
|
||||||
|
.Where(x => x.Item > 0)
|
||||||
|
.GroupBy(x => x.Item)
|
||||||
|
.ToDictionary(
|
||||||
|
g => g.Key,
|
||||||
|
g => g.OrderByDescending(x => x.UpdatedAt).ThenByDescending(x => x.CreatedAt).First());
|
||||||
|
|
||||||
|
return mobileLines
|
||||||
|
.Select(line =>
|
||||||
|
{
|
||||||
|
var numeroNormalizado = MveAuditNormalization.OnlyDigits(line.Linha);
|
||||||
|
vigenciaByLine.TryGetValue(numeroNormalizado, out var vigencia);
|
||||||
|
if (vigencia == null && line.Item > 0)
|
||||||
|
{
|
||||||
|
vigenciaByItem.TryGetValue(line.Item, out vigencia);
|
||||||
|
}
|
||||||
|
|
||||||
|
userDataByLine.TryGetValue(numeroNormalizado, out var userData);
|
||||||
|
if (userData == null && line.Item > 0)
|
||||||
|
{
|
||||||
|
userDataByItem.TryGetValue(line.Item, out userData);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new MveSystemLineAggregate(line, vigencia, userData, numeroNormalizado);
|
||||||
|
})
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MveReconciliationIssueResult? CompareMatchedLine(
|
||||||
|
MveSystemLineAggregate systemLine,
|
||||||
|
MveParsedLine reportLine)
|
||||||
|
{
|
||||||
|
var systemSnapshot = BuildSystemSnapshot(systemLine);
|
||||||
|
var reportSnapshot = BuildReportSnapshot(reportLine);
|
||||||
|
var differences = new List<MveAuditDifferenceDto>();
|
||||||
|
|
||||||
|
var systemStatus = MveAuditNormalization.NormalizeSystemStatus(systemSnapshot.StatusLinha);
|
||||||
|
if (!string.Equals(systemStatus.Key, reportLine.StatusLinhaKey, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
differences.Add(new MveAuditDifferenceDto
|
||||||
|
{
|
||||||
|
FieldKey = "status",
|
||||||
|
Label = "Status da linha",
|
||||||
|
SystemValue = NullIfEmpty(systemSnapshot.StatusLinha),
|
||||||
|
ReportValue = NullIfEmpty(reportSnapshot.StatusLinha),
|
||||||
|
Syncable = true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
AddDifference(
|
||||||
|
differences,
|
||||||
|
"chip",
|
||||||
|
"Chip da linha",
|
||||||
|
systemSnapshot.Chip,
|
||||||
|
reportSnapshot.Chip,
|
||||||
|
syncable: true,
|
||||||
|
comparer: MveAuditNormalization.OnlyDigits);
|
||||||
|
|
||||||
|
var hasUnknownStatus = !reportLine.StatusLinhaRecognized;
|
||||||
|
if (differences.Count == 0 && !hasUnknownStatus)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return BuildIssue(systemLine, reportLine, systemSnapshot, reportSnapshot, differences, hasUnknownStatus);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MveReconciliationIssueResult? CompareMatchedByChip(
|
||||||
|
MveSystemLineAggregate systemLine,
|
||||||
|
MveParsedLine reportLine)
|
||||||
|
{
|
||||||
|
var systemSnapshot = BuildSystemSnapshot(systemLine);
|
||||||
|
var reportSnapshot = BuildReportSnapshot(reportLine);
|
||||||
|
var systemLocalNumber = MveAuditNormalization.ExtractPhoneLocalNumber(systemSnapshot.NumeroLinha);
|
||||||
|
var reportLocalNumber = MveAuditNormalization.ExtractPhoneLocalNumber(reportSnapshot.NumeroLinha);
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(systemLocalNumber) &&
|
||||||
|
string.Equals(systemLocalNumber, reportLocalNumber, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return BuildDddReviewIssue(systemLine, reportLine, systemSnapshot, reportSnapshot);
|
||||||
|
}
|
||||||
|
|
||||||
|
var differences = new List<MveAuditDifferenceDto>();
|
||||||
|
AddDifference(
|
||||||
|
differences,
|
||||||
|
"line",
|
||||||
|
"Número da linha",
|
||||||
|
systemSnapshot.NumeroLinha,
|
||||||
|
reportSnapshot.NumeroLinha,
|
||||||
|
syncable: true,
|
||||||
|
comparer: MveAuditNormalization.OnlyDigits);
|
||||||
|
|
||||||
|
var systemStatus = MveAuditNormalization.NormalizeSystemStatus(systemSnapshot.StatusLinha);
|
||||||
|
if (!string.Equals(systemStatus.Key, reportLine.StatusLinhaKey, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
differences.Add(new MveAuditDifferenceDto
|
||||||
|
{
|
||||||
|
FieldKey = "status",
|
||||||
|
Label = "Status da linha",
|
||||||
|
SystemValue = NullIfEmpty(systemSnapshot.StatusLinha),
|
||||||
|
ReportValue = NullIfEmpty(reportSnapshot.StatusLinha),
|
||||||
|
Syncable = true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var hasUnknownStatus = !reportLine.StatusLinhaRecognized;
|
||||||
|
if (differences.Count == 0 && !hasUnknownStatus)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return BuildIssue(systemLine, reportLine, systemSnapshot, reportSnapshot, differences, hasUnknownStatus);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void RegisterComparisonResult(MveReconciliationResult result, MveReconciliationIssueResult? comparison)
|
||||||
|
{
|
||||||
|
if (comparison == null)
|
||||||
|
{
|
||||||
|
result.TotalConciliated++;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Issues.Add(comparison);
|
||||||
|
if (comparison.Differences.Any(x => x.FieldKey == "status"))
|
||||||
|
{
|
||||||
|
result.TotalStatusDivergences++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (comparison.Differences.Any(x => x.FieldKey != "status" && x.Syncable))
|
||||||
|
{
|
||||||
|
result.TotalDataDivergences++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (comparison.Syncable)
|
||||||
|
{
|
||||||
|
result.TotalSyncableIssues++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MveReconciliationIssueResult BuildIssue(
|
||||||
|
MveSystemLineAggregate systemLine,
|
||||||
|
MveParsedLine reportLine,
|
||||||
|
MveAuditSnapshotDto systemSnapshot,
|
||||||
|
MveAuditSnapshotDto reportSnapshot,
|
||||||
|
List<MveAuditDifferenceDto> differences,
|
||||||
|
bool hasUnknownStatus)
|
||||||
|
{
|
||||||
|
var notes = new List<string>();
|
||||||
|
if (hasUnknownStatus)
|
||||||
|
{
|
||||||
|
notes.Add("O STATUS_LINHA do relatório MVE não foi reconhecido pelo mapa de normalização.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var hasStatusDifference = differences.Any(x => x.FieldKey == "status");
|
||||||
|
var hasLineDifference = differences.Any(x => x.FieldKey == "line");
|
||||||
|
var hasChipDifference = differences.Any(x => x.FieldKey == "chip");
|
||||||
|
var hasDataDifference = differences.Any(x => x.FieldKey != "status" && x.Syncable);
|
||||||
|
|
||||||
|
return new MveReconciliationIssueResult
|
||||||
|
{
|
||||||
|
SourceRowNumber = reportLine.SourceRowNumber,
|
||||||
|
NumeroLinha = reportLine.NumeroNormalizado,
|
||||||
|
MobileLineId = systemLine.MobileLine.Id,
|
||||||
|
SystemItem = systemLine.MobileLine.Item,
|
||||||
|
IssueType = ResolveIssueType(hasStatusDifference, hasLineDifference, hasChipDifference, hasUnknownStatus),
|
||||||
|
Situation = ResolveSituation(hasStatusDifference, hasLineDifference, hasChipDifference, hasUnknownStatus),
|
||||||
|
Severity = ResolveSeverity(hasStatusDifference, hasDataDifference, hasUnknownStatus),
|
||||||
|
Syncable = differences.Any(x => x.Syncable),
|
||||||
|
ActionSuggestion = ResolveActionSuggestion(hasStatusDifference, hasLineDifference, hasChipDifference, hasUnknownStatus),
|
||||||
|
Notes = notes.Count == 0 ? null : string.Join(" ", notes),
|
||||||
|
SystemStatus = systemSnapshot.StatusLinha,
|
||||||
|
ReportStatus = reportSnapshot.StatusLinha,
|
||||||
|
SystemPlan = systemSnapshot.PlanoLinha,
|
||||||
|
ReportPlan = reportSnapshot.PlanoLinha,
|
||||||
|
SystemSnapshot = systemSnapshot,
|
||||||
|
ReportSnapshot = reportSnapshot,
|
||||||
|
Differences = differences
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MveReconciliationIssueResult BuildDddReviewIssue(
|
||||||
|
MveSystemLineAggregate systemLine,
|
||||||
|
MveParsedLine reportLine,
|
||||||
|
MveAuditSnapshotDto systemSnapshot,
|
||||||
|
MveAuditSnapshotDto reportSnapshot)
|
||||||
|
{
|
||||||
|
return new MveReconciliationIssueResult
|
||||||
|
{
|
||||||
|
SourceRowNumber = reportLine.SourceRowNumber,
|
||||||
|
NumeroLinha = reportLine.NumeroNormalizado,
|
||||||
|
MobileLineId = systemLine.MobileLine.Id,
|
||||||
|
SystemItem = systemLine.MobileLine.Item,
|
||||||
|
IssueType = "DDD_CHANGE_REVIEW",
|
||||||
|
Situation = "mudança de DDD detectada",
|
||||||
|
Severity = "WARNING",
|
||||||
|
Syncable = false,
|
||||||
|
ActionSuggestion = "Revisar manualmente na página Mureg antes de aplicar alterações",
|
||||||
|
Notes = "O mesmo chip foi encontrado com o mesmo número base, mas com DDD diferente. Esse cenário ainda não é atualizado automaticamente pelo MVE.",
|
||||||
|
SystemStatus = systemSnapshot.StatusLinha,
|
||||||
|
ReportStatus = reportSnapshot.StatusLinha,
|
||||||
|
SystemPlan = systemSnapshot.PlanoLinha,
|
||||||
|
ReportPlan = reportSnapshot.PlanoLinha,
|
||||||
|
SystemSnapshot = systemSnapshot,
|
||||||
|
ReportSnapshot = reportSnapshot,
|
||||||
|
Differences = new List<MveAuditDifferenceDto>
|
||||||
|
{
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
FieldKey = "ddd",
|
||||||
|
Label = "DDD da linha",
|
||||||
|
SystemValue = NullIfEmpty(MveAuditNormalization.ExtractPhoneDdd(systemSnapshot.NumeroLinha)),
|
||||||
|
ReportValue = NullIfEmpty(MveAuditNormalization.ExtractPhoneDdd(reportSnapshot.NumeroLinha)),
|
||||||
|
Syncable = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ResolveIssueType(
|
||||||
|
bool hasStatusDifference,
|
||||||
|
bool hasLineDifference,
|
||||||
|
bool hasChipDifference,
|
||||||
|
bool hasUnknownStatus)
|
||||||
|
{
|
||||||
|
if (hasLineDifference)
|
||||||
|
{
|
||||||
|
return "LINE_CHANGE_DETECTED";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasChipDifference)
|
||||||
|
{
|
||||||
|
return "CHIP_CHANGE_DETECTED";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasStatusDifference)
|
||||||
|
{
|
||||||
|
return "STATUS_DIVERGENCE";
|
||||||
|
}
|
||||||
|
|
||||||
|
return hasUnknownStatus ? "UNKNOWN_STATUS" : "ALIGNED";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ResolveSituation(
|
||||||
|
bool hasStatusDifference,
|
||||||
|
bool hasLineDifference,
|
||||||
|
bool hasChipDifference,
|
||||||
|
bool hasUnknownStatus)
|
||||||
|
{
|
||||||
|
if (hasLineDifference && hasStatusDifference)
|
||||||
|
{
|
||||||
|
return "troca de número e status diferente";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasLineDifference)
|
||||||
|
{
|
||||||
|
return "troca de número detectada";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasChipDifference && hasStatusDifference)
|
||||||
|
{
|
||||||
|
return "troca de chip e status diferente";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasChipDifference)
|
||||||
|
{
|
||||||
|
return "troca de chip detectada";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasStatusDifference)
|
||||||
|
{
|
||||||
|
return "divergência de status";
|
||||||
|
}
|
||||||
|
|
||||||
|
return hasUnknownStatus ? "status desconhecido no relatório" : "alinhada";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ResolveSeverity(bool hasStatusDifference, bool hasDataDifference, bool hasUnknownStatus)
|
||||||
|
{
|
||||||
|
if (hasStatusDifference)
|
||||||
|
{
|
||||||
|
return "HIGH";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasDataDifference)
|
||||||
|
{
|
||||||
|
return "MEDIUM";
|
||||||
|
}
|
||||||
|
|
||||||
|
return hasUnknownStatus ? "WARNING" : "INFO";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ResolveActionSuggestion(
|
||||||
|
bool hasStatusDifference,
|
||||||
|
bool hasLineDifference,
|
||||||
|
bool hasChipDifference,
|
||||||
|
bool hasUnknownStatus)
|
||||||
|
{
|
||||||
|
if (hasLineDifference && hasStatusDifference)
|
||||||
|
{
|
||||||
|
return "Atualizar linha e status da linha com base no MVE";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasLineDifference)
|
||||||
|
{
|
||||||
|
return "Atualizar a linha cadastrada com base no chip informado no MVE";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasChipDifference && hasStatusDifference)
|
||||||
|
{
|
||||||
|
return "Atualizar chip e status da linha com base no MVE";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasChipDifference)
|
||||||
|
{
|
||||||
|
return "Atualizar o chip da linha com base no MVE";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasStatusDifference)
|
||||||
|
{
|
||||||
|
return "Atualizar status da linha com base no MVE";
|
||||||
|
}
|
||||||
|
|
||||||
|
return hasUnknownStatus
|
||||||
|
? "Revisar o status recebido e ajustar o mapa de normalização se necessário"
|
||||||
|
: "Nenhuma ação";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MveAuditSnapshotDto BuildSystemSnapshot(MveSystemLineAggregate systemLine)
|
||||||
|
{
|
||||||
|
return new MveAuditSnapshotDto
|
||||||
|
{
|
||||||
|
NumeroLinha = NullIfEmpty(systemLine.MobileLine.Linha),
|
||||||
|
StatusLinha = NullIfEmpty(systemLine.MobileLine.Status),
|
||||||
|
PlanoLinha = NullIfEmpty(systemLine.MobileLine.PlanoContrato),
|
||||||
|
DataAtivacao = systemLine.Vigencia?.DtEfetivacaoServico,
|
||||||
|
TerminoContrato = systemLine.Vigencia?.DtTerminoFidelizacao,
|
||||||
|
Chip = NullIfEmpty(systemLine.MobileLine.Chip),
|
||||||
|
Conta = NullIfEmpty(systemLine.MobileLine.Conta),
|
||||||
|
Cnpj = NullIfEmpty(systemLine.UserData?.Cnpj),
|
||||||
|
ModeloAparelho = NullIfEmpty(systemLine.MobileLine.Aparelho?.Nome),
|
||||||
|
Fabricante = NullIfEmpty(systemLine.MobileLine.Aparelho?.Fabricante)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MveAuditSnapshotDto BuildReportSnapshot(MveParsedLine reportLine)
|
||||||
|
{
|
||||||
|
return new MveAuditSnapshotDto
|
||||||
|
{
|
||||||
|
NumeroLinha = NullIfEmpty(reportLine.NumeroNormalizado),
|
||||||
|
StatusLinha = NullIfEmpty(reportLine.StatusLinha),
|
||||||
|
StatusConta = NullIfEmpty(reportLine.StatusConta),
|
||||||
|
PlanoLinha = NullIfEmpty(reportLine.PlanoLinha),
|
||||||
|
DataAtivacao = reportLine.DataAtivacao,
|
||||||
|
TerminoContrato = reportLine.TerminoContrato,
|
||||||
|
Chip = NullIfEmpty(reportLine.Chip),
|
||||||
|
Conta = NullIfEmpty(reportLine.Conta),
|
||||||
|
Cnpj = NullIfEmpty(reportLine.Cnpj),
|
||||||
|
ModeloAparelho = NullIfEmpty(reportLine.ModeloAparelho),
|
||||||
|
Fabricante = NullIfEmpty(reportLine.Fabricante),
|
||||||
|
ServicosAtivos = reportLine.ServicosAtivos
|
||||||
|
.Where(x => !string.IsNullOrWhiteSpace(x))
|
||||||
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToList()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AddDifference(
|
||||||
|
ICollection<MveAuditDifferenceDto> differences,
|
||||||
|
string fieldKey,
|
||||||
|
string label,
|
||||||
|
string? systemValue,
|
||||||
|
string? reportValue,
|
||||||
|
bool syncable,
|
||||||
|
Func<string?, string> comparer)
|
||||||
|
{
|
||||||
|
var normalizedSystem = comparer(systemValue);
|
||||||
|
var normalizedReport = comparer(reportValue);
|
||||||
|
if (string.Equals(normalizedSystem, normalizedReport, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
differences.Add(new MveAuditDifferenceDto
|
||||||
|
{
|
||||||
|
FieldKey = fieldKey,
|
||||||
|
Label = label,
|
||||||
|
SystemValue = NullIfEmpty(systemValue),
|
||||||
|
ReportValue = NullIfEmpty(reportValue),
|
||||||
|
Syncable = syncable
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? NullIfEmpty(string? value)
|
||||||
|
{
|
||||||
|
return string.IsNullOrWhiteSpace(value) ? null : value.Trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class MveReconciliationResult
|
||||||
|
{
|
||||||
|
public int TotalSystemLines { get; init; }
|
||||||
|
|
||||||
|
public int TotalReportLines { get; init; }
|
||||||
|
|
||||||
|
public int TotalConciliated { get; set; }
|
||||||
|
|
||||||
|
public int TotalStatusDivergences { get; set; }
|
||||||
|
|
||||||
|
public int TotalDataDivergences { get; set; }
|
||||||
|
|
||||||
|
public int TotalOnlyInSystem { get; set; }
|
||||||
|
|
||||||
|
public int TotalOnlyInReport { get; set; }
|
||||||
|
|
||||||
|
public int TotalDuplicateReportLines { get; set; }
|
||||||
|
|
||||||
|
public int TotalDuplicateSystemLines { get; set; }
|
||||||
|
|
||||||
|
public int TotalInvalidRows { get; init; }
|
||||||
|
|
||||||
|
public int TotalUnknownStatuses { get; init; }
|
||||||
|
|
||||||
|
public int TotalSyncableIssues { get; set; }
|
||||||
|
|
||||||
|
public List<MveReconciliationIssueResult> Issues { get; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class MveReconciliationIssueResult
|
||||||
|
{
|
||||||
|
public int? SourceRowNumber { get; init; }
|
||||||
|
|
||||||
|
public string NumeroLinha { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
public Guid? MobileLineId { get; init; }
|
||||||
|
|
||||||
|
public int? SystemItem { get; init; }
|
||||||
|
|
||||||
|
public string IssueType { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
public string Situation { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
public string Severity { get; init; } = "INFO";
|
||||||
|
|
||||||
|
public bool Syncable { get; init; }
|
||||||
|
|
||||||
|
public string? ActionSuggestion { get; init; }
|
||||||
|
|
||||||
|
public string? Notes { get; init; }
|
||||||
|
|
||||||
|
public string? SystemStatus { get; init; }
|
||||||
|
|
||||||
|
public string? ReportStatus { get; init; }
|
||||||
|
|
||||||
|
public string? SystemPlan { get; init; }
|
||||||
|
|
||||||
|
public string? ReportPlan { get; init; }
|
||||||
|
|
||||||
|
public MveAuditSnapshotDto? SystemSnapshot { get; init; }
|
||||||
|
|
||||||
|
public MveAuditSnapshotDto? ReportSnapshot { get; init; }
|
||||||
|
|
||||||
|
public List<MveAuditDifferenceDto> Differences { get; init; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed record MveSystemLineAggregate(
|
||||||
|
MobileLine MobileLine,
|
||||||
|
VigenciaLine? Vigencia,
|
||||||
|
UserData? UserData,
|
||||||
|
string NumeroNormalizado);
|
||||||
|
|
@ -0,0 +1,260 @@
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Text;
|
||||||
|
using line_gestao_api.Dtos;
|
||||||
|
using line_gestao_api.Models;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace line_gestao_api.Services
|
||||||
|
{
|
||||||
|
public sealed record OperadoraContaContext(string Operadora, string Empresa, string? VivoEmpresaGrupo = null);
|
||||||
|
|
||||||
|
public static class OperadoraContaResolver
|
||||||
|
{
|
||||||
|
private const string OperadoraVivo = "VIVO";
|
||||||
|
private const string OperadoraClaro = "CLARO";
|
||||||
|
private const string OperadoraTim = "TIM";
|
||||||
|
private const string OperadoraOutra = "OUTRA";
|
||||||
|
|
||||||
|
private const string EmpresaVivoMacrophony = "VIVO MACROPHONY";
|
||||||
|
private const string EmpresaVivoLineMovel = "VIVO LINE MÓVEL";
|
||||||
|
private const string EmpresaClaroLineMovel = "CLARO LINE MÓVEL";
|
||||||
|
private const string EmpresaTimLineMovel = "TIM LINE MÓVEL";
|
||||||
|
|
||||||
|
private static readonly IReadOnlyList<AccountCompanyDto> CompanyRules = new List<AccountCompanyDto>
|
||||||
|
{
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Empresa = EmpresaClaroLineMovel,
|
||||||
|
Contas = new List<string> { "172593311", "172593840", "187890982" }
|
||||||
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Empresa = EmpresaVivoMacrophony,
|
||||||
|
Contas = new List<string>
|
||||||
|
{
|
||||||
|
"0430237019",
|
||||||
|
"0437488125",
|
||||||
|
"0449508564",
|
||||||
|
"0454371844",
|
||||||
|
"455371844",
|
||||||
|
"460161507"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Empresa = EmpresaVivoLineMovel,
|
||||||
|
Contas = new List<string> { "0435288088" }
|
||||||
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Empresa = EmpresaTimLineMovel,
|
||||||
|
Contas = new List<string> { "TIM" }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private static readonly string[] VivoContaFilters = BuildContaFilterSet(EmpresaVivoMacrophony, EmpresaVivoLineMovel);
|
||||||
|
private static readonly string[] ClaroContaFilters = BuildContaFilterSet(EmpresaClaroLineMovel);
|
||||||
|
private static readonly string[] TimContaFilters = BuildContaFilterSet(EmpresaTimLineMovel);
|
||||||
|
private static readonly Dictionary<string, string> EmpresaByConta = BuildEmpresaByConta();
|
||||||
|
|
||||||
|
public static List<AccountCompanyDto> GetAccountCompanies()
|
||||||
|
{
|
||||||
|
return CompanyRules
|
||||||
|
.Select(x => new AccountCompanyDto
|
||||||
|
{
|
||||||
|
Empresa = x.Empresa,
|
||||||
|
Contas = x.Contas.ToList()
|
||||||
|
})
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<string> GetAccountsByEmpresa(string? empresa)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(empresa)) return new List<string>();
|
||||||
|
var target = NormalizeToken(empresa);
|
||||||
|
if (string.IsNullOrWhiteSpace(target)) return new List<string>();
|
||||||
|
|
||||||
|
return CompanyRules
|
||||||
|
.FirstOrDefault(x => NormalizeToken(x.Empresa) == target)
|
||||||
|
?.Contas
|
||||||
|
?.ToList()
|
||||||
|
?? new List<string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static OperadoraContaContext Resolve(string? conta)
|
||||||
|
{
|
||||||
|
var contaRaw = (conta ?? string.Empty).Trim();
|
||||||
|
if (string.IsNullOrWhiteSpace(contaRaw))
|
||||||
|
{
|
||||||
|
return new OperadoraContaContext(OperadoraOutra, string.Empty, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
var normalizedConta = NormalizeConta(contaRaw);
|
||||||
|
if (!string.IsNullOrWhiteSpace(normalizedConta) && EmpresaByConta.TryGetValue(normalizedConta, out var empresaDeterministica))
|
||||||
|
{
|
||||||
|
return BuildContextByEmpresa(empresaDeterministica);
|
||||||
|
}
|
||||||
|
|
||||||
|
var token = NormalizeToken(contaRaw);
|
||||||
|
if (token.Contains("TIM", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return BuildContextByEmpresa(EmpresaTimLineMovel);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (token.Contains("CLARO", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return BuildContextByEmpresa(EmpresaClaroLineMovel);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (token.Contains("MACROPHONY", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return BuildContextByEmpresa(EmpresaVivoMacrophony);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (token.Contains("VIVO", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return BuildContextByEmpresa(EmpresaVivoLineMovel);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new OperadoraContaContext(OperadoraOutra, string.Empty, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IQueryable<MobileLine> ApplyOperadoraFilter(IQueryable<MobileLine> query, string? operadora)
|
||||||
|
{
|
||||||
|
var token = NormalizeToken(operadora);
|
||||||
|
if (string.IsNullOrWhiteSpace(token) || token == "TODOS" || token == "ALL")
|
||||||
|
{
|
||||||
|
return query;
|
||||||
|
}
|
||||||
|
|
||||||
|
return token switch
|
||||||
|
{
|
||||||
|
OperadoraVivo => query.Where(x =>
|
||||||
|
(x.Conta != null && VivoContaFilters.Contains(x.Conta.Trim()))
|
||||||
|
|| EF.Functions.ILike((x.Conta ?? string.Empty).Trim(), "%VIVO%")
|
||||||
|
|| EF.Functions.ILike((x.Conta ?? string.Empty).Trim(), "%MACROPHONY%")),
|
||||||
|
OperadoraClaro => query.Where(x =>
|
||||||
|
(x.Conta != null && ClaroContaFilters.Contains(x.Conta.Trim()))
|
||||||
|
|| EF.Functions.ILike((x.Conta ?? string.Empty).Trim(), "%CLARO%")),
|
||||||
|
OperadoraTim => query.Where(x =>
|
||||||
|
(x.Conta != null && TimContaFilters.Contains(x.Conta.Trim()))
|
||||||
|
|| EF.Functions.ILike((x.Conta ?? string.Empty).Trim(), "%TIM%")),
|
||||||
|
_ => query
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string NormalizeConta(string? conta)
|
||||||
|
{
|
||||||
|
var raw = (conta ?? string.Empty).Trim();
|
||||||
|
if (string.IsNullOrWhiteSpace(raw)) return string.Empty;
|
||||||
|
|
||||||
|
if (raw.All(char.IsDigit))
|
||||||
|
{
|
||||||
|
var noLeadingZero = raw.TrimStart('0');
|
||||||
|
return string.IsNullOrWhiteSpace(noLeadingZero) ? "0" : noLeadingZero;
|
||||||
|
}
|
||||||
|
|
||||||
|
return RemoveDiacritics(raw).ToUpperInvariant();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static OperadoraContaContext BuildContextByEmpresa(string empresa)
|
||||||
|
{
|
||||||
|
var token = NormalizeToken(empresa);
|
||||||
|
var operadora = token.Contains("CLARO", StringComparison.Ordinal)
|
||||||
|
? OperadoraClaro
|
||||||
|
: token.Contains("TIM", StringComparison.Ordinal)
|
||||||
|
? OperadoraTim
|
||||||
|
: token.Contains("VIVO", StringComparison.Ordinal) || token.Contains("MACROPHONY", StringComparison.Ordinal)
|
||||||
|
? OperadoraVivo
|
||||||
|
: OperadoraOutra;
|
||||||
|
|
||||||
|
var vivoGrupo = operadora == OperadoraVivo
|
||||||
|
? token.Contains("MACROPHONY", StringComparison.Ordinal)
|
||||||
|
? "MACROPHONY"
|
||||||
|
: token.Contains("LINEMOVEL", StringComparison.Ordinal)
|
||||||
|
? "LINE MOVEL"
|
||||||
|
: null
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return new OperadoraContaContext(operadora, empresa, vivoGrupo);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Dictionary<string, string> BuildEmpresaByConta()
|
||||||
|
{
|
||||||
|
var map = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
foreach (var group in CompanyRules)
|
||||||
|
{
|
||||||
|
foreach (var conta in group.Contas ?? new List<string>())
|
||||||
|
{
|
||||||
|
var normalized = NormalizeConta(conta);
|
||||||
|
if (string.IsNullOrWhiteSpace(normalized)) continue;
|
||||||
|
map[normalized] = group.Empresa;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string[] BuildContaFilterSet(params string[] empresas)
|
||||||
|
{
|
||||||
|
var empresaTokens = new HashSet<string>(empresas.Select(NormalizeToken), StringComparer.OrdinalIgnoreCase);
|
||||||
|
var set = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
foreach (var group in CompanyRules)
|
||||||
|
{
|
||||||
|
if (!empresaTokens.Contains(NormalizeToken(group.Empresa))) continue;
|
||||||
|
|
||||||
|
foreach (var conta in group.Contas ?? new List<string>())
|
||||||
|
{
|
||||||
|
var trimmed = (conta ?? string.Empty).Trim();
|
||||||
|
if (string.IsNullOrWhiteSpace(trimmed)) continue;
|
||||||
|
|
||||||
|
set.Add(trimmed);
|
||||||
|
|
||||||
|
if (trimmed.All(char.IsDigit))
|
||||||
|
{
|
||||||
|
var noLeading = trimmed.TrimStart('0');
|
||||||
|
if (!string.IsNullOrWhiteSpace(noLeading))
|
||||||
|
{
|
||||||
|
set.Add(noLeading);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return set.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string NormalizeToken(string? value)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(value)) return string.Empty;
|
||||||
|
var raw = RemoveDiacritics(value);
|
||||||
|
var sb = new StringBuilder(raw.Length);
|
||||||
|
foreach (var ch in raw)
|
||||||
|
{
|
||||||
|
if (char.IsLetterOrDigit(ch)) sb.Append(char.ToUpperInvariant(ch));
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string RemoveDiacritics(string value)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(value)) return string.Empty;
|
||||||
|
|
||||||
|
var normalized = value.Normalize(NormalizationForm.FormD);
|
||||||
|
var sb = new StringBuilder(normalized.Length);
|
||||||
|
foreach (var c in normalized)
|
||||||
|
{
|
||||||
|
var category = CharUnicodeInfo.GetUnicodeCategory(c);
|
||||||
|
if (category != UnicodeCategory.NonSpacingMark)
|
||||||
|
{
|
||||||
|
sb.Append(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.ToString().Normalize(NormalizationForm.FormC);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -19,7 +19,8 @@ public class TenantProvider : ITenantProvider
|
||||||
|
|
||||||
public bool HasGlobalViewAccess =>
|
public bool HasGlobalViewAccess =>
|
||||||
HasRole(AppRoles.SysAdmin) ||
|
HasRole(AppRoles.SysAdmin) ||
|
||||||
HasRole(AppRoles.Gestor);
|
HasRole(AppRoles.Gestor) ||
|
||||||
|
HasRole(AppRoles.Financeiro);
|
||||||
|
|
||||||
public void SetTenantId(Guid? tenantId)
|
public void SetTenantId(Guid? tenantId)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,8 @@ public class VigenciaNotificationBackgroundService : BackgroundService
|
||||||
var intervalMinutes = _options.CheckIntervalMinutes <= 0 ? 60 : _options.CheckIntervalMinutes;
|
var intervalMinutes = _options.CheckIntervalMinutes <= 0 ? 60 : _options.CheckIntervalMinutes;
|
||||||
using var timer = new PeriodicTimer(TimeSpan.FromMinutes(intervalMinutes));
|
using var timer = new PeriodicTimer(TimeSpan.FromMinutes(intervalMinutes));
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
await RunOnceAsync(stoppingToken);
|
await RunOnceAsync(stoppingToken);
|
||||||
|
|
||||||
while (await timer.WaitForNextTickAsync(stoppingToken))
|
while (await timer.WaitForNextTickAsync(stoppingToken))
|
||||||
|
|
@ -34,6 +36,11 @@ public class VigenciaNotificationBackgroundService : BackgroundService
|
||||||
await RunOnceAsync(stoppingToken);
|
await RunOnceAsync(stoppingToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
// Host finalizando; evita ruído de log durante shutdown/startup interrompido.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async Task RunOnceAsync(CancellationToken stoppingToken)
|
private async Task RunOnceAsync(CancellationToken stoppingToken)
|
||||||
{
|
{
|
||||||
|
|
@ -64,7 +71,11 @@ public class VigenciaNotificationBackgroundService : BackgroundService
|
||||||
await notificationSyncService.SyncTenantAsync(tenant.Id, stoppingToken);
|
await notificationSyncService.SyncTenantAsync(tenant.Id, stoppingToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
// Host finalizando; evita erro em cascata no logger.
|
||||||
|
}
|
||||||
|
catch (Exception ex) when (!stoppingToken.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "Erro ao gerar notificações de vigência.");
|
_logger.LogError(ex, "Erro ao gerar notificações de vigência.");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,11 @@ using line_gestao_api.Data;
|
||||||
using line_gestao_api.Dtos;
|
using line_gestao_api.Dtos;
|
||||||
using line_gestao_api.Models;
|
using line_gestao_api.Models;
|
||||||
using line_gestao_api.Services;
|
using line_gestao_api.Services;
|
||||||
|
using Microsoft.AspNetCore.Hosting;
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
using Microsoft.AspNetCore.Mvc.Testing;
|
using Microsoft.AspNetCore.Mvc.Testing;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||||
|
|
@ -324,6 +326,7 @@ public class SystemTenantIntegrationTests
|
||||||
|
|
||||||
services.RemoveAll<AppDbContext>();
|
services.RemoveAll<AppDbContext>();
|
||||||
services.RemoveAll<DbContextOptions<AppDbContext>>();
|
services.RemoveAll<DbContextOptions<AppDbContext>>();
|
||||||
|
services.RemoveAll<IDbContextOptionsConfiguration<AppDbContext>>();
|
||||||
|
|
||||||
services.AddDbContext<AppDbContext>(options =>
|
services.AddDbContext<AppDbContext>(options =>
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 41 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loading…
Reference in New Issue