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 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(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 BuildSystemAggregates( IReadOnlyCollection mobileLines, IReadOnlyCollection vigencias, IReadOnlyCollection 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(); 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(); 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 differences, string fieldKey, string label, string? systemValue, string? reportValue, bool syncable, Func 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 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 Differences { get; init; } = new(); } internal sealed record MveSystemLineAggregate( MobileLine MobileLine, VigenciaLine? Vigencia, UserData? UserData, string NumeroNormalizado);