521 lines
19 KiB
C#
521 lines
19 KiB
C#
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 blockedKeys = new HashSet<string>(duplicateReportKeys, StringComparer.Ordinal);
|
|
blockedKeys.UnionWith(duplicateSystemKeys);
|
|
|
|
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 (blockedKeys.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)
|
|
{
|
|
result.TotalOnlyInSystem++;
|
|
result.Issues.Add(new MveReconciliationIssueResult
|
|
{
|
|
NumeroLinha = key,
|
|
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)
|
|
});
|
|
continue;
|
|
}
|
|
|
|
if (reportLine != null && systemLine == null)
|
|
{
|
|
result.TotalOnlyInReport++;
|
|
result.Issues.Add(new MveReconciliationIssueResult
|
|
{
|
|
SourceRowNumber = reportLine.SourceRowNumber,
|
|
NumeroLinha = key,
|
|
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)
|
|
});
|
|
continue;
|
|
}
|
|
|
|
if (reportLine == null || systemLine == null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var comparison = CompareMatchedLine(systemLine, reportLine);
|
|
if (comparison == null)
|
|
{
|
|
result.TotalConciliated++;
|
|
continue;
|
|
}
|
|
|
|
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++;
|
|
}
|
|
}
|
|
|
|
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
|
|
});
|
|
}
|
|
|
|
var hasUnknownStatus = !reportLine.StatusLinhaRecognized;
|
|
if (differences.Count == 0 && !hasUnknownStatus)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
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 hasDataDifference = false;
|
|
var issueType = hasStatusDifference ? "STATUS_DIVERGENCE" : "UNKNOWN_STATUS";
|
|
|
|
return new MveReconciliationIssueResult
|
|
{
|
|
SourceRowNumber = reportLine.SourceRowNumber,
|
|
NumeroLinha = reportLine.NumeroNormalizado,
|
|
MobileLineId = systemLine.MobileLine.Id,
|
|
SystemItem = systemLine.MobileLine.Item,
|
|
IssueType = issueType,
|
|
Situation = ResolveSituation(hasStatusDifference, hasDataDifference, hasUnknownStatus),
|
|
Severity = ResolveSeverity(hasStatusDifference, hasDataDifference, hasUnknownStatus),
|
|
Syncable = differences.Any(x => x.Syncable),
|
|
ActionSuggestion = ResolveActionSuggestion(hasStatusDifference, hasDataDifference, 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 string ResolveSituation(bool hasStatusDifference, bool hasDataDifference, bool hasUnknownStatus)
|
|
{
|
|
if (hasStatusDifference && hasDataDifference)
|
|
{
|
|
return "divergência de status e cadastro";
|
|
}
|
|
|
|
if (hasStatusDifference)
|
|
{
|
|
return "divergência de status";
|
|
}
|
|
|
|
if (hasDataDifference)
|
|
{
|
|
return "divergência de cadastro";
|
|
}
|
|
|
|
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 hasDataDifference, bool hasUnknownStatus)
|
|
{
|
|
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);
|