line-gestao-api/Services/MveReconciliationService.cs

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);