Minha alteração
This commit is contained in:
parent
0c17b5e48a
commit
514c7ba8cd
|
|
@ -1,6 +1,7 @@
|
|||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
using line_gestao_api.Data;
|
||||
using line_gestao_api.Dtos;
|
||||
using line_gestao_api.Models;
|
||||
using line_gestao_api.Services;
|
||||
|
|
@ -17,15 +18,18 @@ namespace line_gestao_api.Controllers;
|
|||
public class AuthController : ControllerBase
|
||||
{
|
||||
private readonly UserManager<ApplicationUser> _userManager;
|
||||
private readonly AppDbContext _db;
|
||||
private readonly ITenantProvider _tenantProvider;
|
||||
private readonly IConfiguration _config;
|
||||
|
||||
public AuthController(
|
||||
UserManager<ApplicationUser> userManager,
|
||||
AppDbContext db,
|
||||
ITenantProvider tenantProvider,
|
||||
IConfiguration config)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_db = db;
|
||||
_tenantProvider = tenantProvider;
|
||||
_config = config;
|
||||
}
|
||||
|
|
@ -38,7 +42,7 @@ public class AuthController : ControllerBase
|
|||
return BadRequest("As senhas não conferem.");
|
||||
|
||||
var tenantId = _tenantProvider.TenantId;
|
||||
if (tenantId == null)
|
||||
if (tenantId == null || tenantId == Guid.Empty)
|
||||
return Unauthorized("Tenant inválido.");
|
||||
|
||||
var email = req.Email.Trim().ToLowerInvariant();
|
||||
|
|
@ -65,7 +69,11 @@ public class AuthController : ControllerBase
|
|||
|
||||
await _userManager.AddToRoleAsync(user, "leitura");
|
||||
|
||||
var token = await GenerateJwtAsync(user);
|
||||
var effectiveTenantId = await EnsureValidTenantIdAsync(user);
|
||||
if (!effectiveTenantId.HasValue)
|
||||
return Unauthorized("Tenant inválido.");
|
||||
|
||||
var token = await GenerateJwtAsync(user, effectiveTenantId.Value);
|
||||
return Ok(new AuthResponse(token));
|
||||
}
|
||||
|
||||
|
|
@ -111,7 +119,11 @@ public class AuthController : ControllerBase
|
|||
if (!valid)
|
||||
return Unauthorized("Credenciais inválidas.");
|
||||
|
||||
var token = await GenerateJwtAsync(user);
|
||||
var effectiveTenantId = await EnsureValidTenantIdAsync(user);
|
||||
if (!effectiveTenantId.HasValue)
|
||||
return Unauthorized("Tenant inválido.");
|
||||
|
||||
var token = await GenerateJwtAsync(user, effectiveTenantId.Value);
|
||||
return Ok(new AuthResponse(token));
|
||||
}
|
||||
|
||||
|
|
@ -124,7 +136,7 @@ public class AuthController : ControllerBase
|
|||
return Guid.TryParse(headerValue, out var parsed) ? parsed : null;
|
||||
}
|
||||
|
||||
private async Task<string> GenerateJwtAsync(ApplicationUser user)
|
||||
private async Task<string> GenerateJwtAsync(ApplicationUser user, Guid tenantId)
|
||||
{
|
||||
var key = _config["Jwt:Key"]!;
|
||||
var issuer = _config["Jwt:Issuer"]!;
|
||||
|
|
@ -138,7 +150,7 @@ public class AuthController : ControllerBase
|
|||
new(JwtRegisteredClaimNames.Sub, user.Id.ToString()),
|
||||
new(JwtRegisteredClaimNames.Email, user.Email ?? string.Empty),
|
||||
new("name", user.Name),
|
||||
new("tenantId", user.TenantId.ToString())
|
||||
new("tenantId", tenantId.ToString())
|
||||
};
|
||||
|
||||
claims.AddRange(roles.Select(r => new Claim(ClaimTypes.Role, r)));
|
||||
|
|
@ -156,4 +168,26 @@ public class AuthController : ControllerBase
|
|||
|
||||
return new JwtSecurityTokenHandler().WriteToken(token);
|
||||
}
|
||||
|
||||
private async Task<Guid?> EnsureValidTenantIdAsync(ApplicationUser user)
|
||||
{
|
||||
if (user.TenantId != Guid.Empty)
|
||||
return user.TenantId;
|
||||
|
||||
var fallbackTenantId = await _db.Tenants
|
||||
.AsNoTracking()
|
||||
.OrderBy(t => t.CreatedAt)
|
||||
.Select(t => (Guid?)t.Id)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
if (!fallbackTenantId.HasValue || fallbackTenantId.Value == Guid.Empty)
|
||||
return null;
|
||||
|
||||
user.TenantId = fallbackTenantId.Value;
|
||||
var updateResult = await _userManager.UpdateAsync(user);
|
||||
if (!updateResult.Succeeded)
|
||||
return null;
|
||||
|
||||
return user.TenantId;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ namespace line_gestao_api.Controllers
|
|||
{
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
[Authorize]
|
||||
public class BillingController : ControllerBase
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ namespace line_gestao_api.Controllers
|
|||
{
|
||||
[ApiController]
|
||||
[Route("api/chips-virgens")]
|
||||
[Authorize]
|
||||
public class ChipsVirgensController : ControllerBase
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ namespace line_gestao_api.Controllers
|
|||
{
|
||||
[ApiController]
|
||||
[Route("api/controle-recebidos")]
|
||||
[Authorize]
|
||||
public class ControleRecebidosController : ControllerBase
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
using line_gestao_api.Dtos;
|
||||
using line_gestao_api.Services;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace line_gestao_api.Controllers
|
||||
{
|
||||
[ApiController]
|
||||
[Route("api/dashboard/geral")]
|
||||
[Authorize]
|
||||
public class DashboardGeralController : ControllerBase
|
||||
{
|
||||
private readonly GeralDashboardInsightsService _service;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
using System.Text.Json;
|
||||
using line_gestao_api.Data;
|
||||
using line_gestao_api.Dtos;
|
||||
using line_gestao_api.Services;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
|
@ -34,7 +35,11 @@ public class HistoricoController : ControllerBase
|
|||
page = page < 1 ? 1 : page;
|
||||
pageSize = pageSize < 1 ? 20 : pageSize;
|
||||
|
||||
var q = _db.AuditLogs.AsNoTracking();
|
||||
var q = _db.AuditLogs
|
||||
.AsNoTracking()
|
||||
.Where(x =>
|
||||
!EF.Functions.ILike(x.RequestPath ?? "", "%import-excel%") ||
|
||||
x.Page == AuditLogBuilder.SpreadsheetImportPageName);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(pageName))
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,86 @@
|
|||
using line_gestao_api.Data;
|
||||
using line_gestao_api.Dtos;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace line_gestao_api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/import-audit")]
|
||||
[Authorize]
|
||||
public class ImportAuditController : ControllerBase
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
|
||||
public ImportAuditController(AppDbContext db)
|
||||
{
|
||||
_db = db;
|
||||
}
|
||||
|
||||
[HttpGet("latest")]
|
||||
public async Task<ActionResult<ImportAuditRunDto>> GetLatest()
|
||||
{
|
||||
var run = await _db.ImportAuditRuns
|
||||
.AsNoTracking()
|
||||
.Include(x => x.Issues)
|
||||
.OrderByDescending(x => x.ImportedAt)
|
||||
.ThenByDescending(x => x.Id)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
if (run == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
return Ok(ToDto(run));
|
||||
}
|
||||
|
||||
[HttpGet("{id:guid}")]
|
||||
public async Task<ActionResult<ImportAuditRunDto>> GetById(Guid id)
|
||||
{
|
||||
var run = await _db.ImportAuditRuns
|
||||
.AsNoTracking()
|
||||
.Include(x => x.Issues)
|
||||
.FirstOrDefaultAsync(x => x.Id == id);
|
||||
|
||||
if (run == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
return Ok(ToDto(run));
|
||||
}
|
||||
|
||||
private static ImportAuditRunDto ToDto(Models.ImportAuditRun run)
|
||||
{
|
||||
var issues = run.Issues
|
||||
.OrderBy(x => x.CreatedAt)
|
||||
.ThenBy(x => x.Entity)
|
||||
.Select(x => new ImportAuditIssueDto
|
||||
{
|
||||
Id = x.Id,
|
||||
Entity = x.Entity,
|
||||
FieldName = x.FieldName,
|
||||
SourceValue = x.SourceValue,
|
||||
CanonicalValue = x.CanonicalValue,
|
||||
Resolution = x.Resolution,
|
||||
Severity = x.Severity,
|
||||
CreatedAt = x.CreatedAt
|
||||
})
|
||||
.ToList();
|
||||
|
||||
return new ImportAuditRunDto
|
||||
{
|
||||
Id = run.Id,
|
||||
ImportedAt = run.ImportedAt,
|
||||
FileName = run.FileName,
|
||||
Status = run.Status,
|
||||
CanonicalTotalLinhas = run.CanonicalTotalLinhas,
|
||||
SourceMaxItemGeral = run.SourceMaxItemGeral,
|
||||
SourceValidCountGeral = run.SourceValidCountGeral,
|
||||
IssueCount = issues.Count,
|
||||
Issues = issues
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -7,22 +7,26 @@ using Microsoft.AspNetCore.Authorization;
|
|||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Security.Claims;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace line_gestao_api.Controllers
|
||||
{
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
//[Authorize]
|
||||
[Authorize]
|
||||
public class LinesController : ControllerBase
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
private readonly ITenantProvider _tenantProvider;
|
||||
private readonly ParcelamentosImportService _parcelamentosImportService;
|
||||
private readonly SpreadsheetImportAuditService _spreadsheetImportAuditService;
|
||||
private static readonly List<AccountCompanyDto> AccountCompanies = new()
|
||||
{
|
||||
new AccountCompanyDto
|
||||
|
|
@ -47,10 +51,16 @@ namespace line_gestao_api.Controllers
|
|||
}
|
||||
};
|
||||
|
||||
public LinesController(AppDbContext db, ParcelamentosImportService parcelamentosImportService)
|
||||
public LinesController(
|
||||
AppDbContext db,
|
||||
ITenantProvider tenantProvider,
|
||||
ParcelamentosImportService parcelamentosImportService,
|
||||
SpreadsheetImportAuditService spreadsheetImportAuditService)
|
||||
{
|
||||
_db = db;
|
||||
_tenantProvider = tenantProvider;
|
||||
_parcelamentosImportService = parcelamentosImportService;
|
||||
_spreadsheetImportAuditService = spreadsheetImportAuditService;
|
||||
}
|
||||
|
||||
public class ImportExcelForm
|
||||
|
|
@ -581,6 +591,7 @@ namespace line_gestao_api.Controllers
|
|||
// ✅ 7. DELETE
|
||||
// ==========================================================
|
||||
[HttpDelete("{id:guid}")]
|
||||
[Authorize(Roles = "admin")]
|
||||
public async Task<IActionResult> Delete(Guid id)
|
||||
{
|
||||
var x = await _db.MobileLines.FirstOrDefaultAsync(a => a.Id == id);
|
||||
|
|
@ -599,10 +610,15 @@ namespace line_gestao_api.Controllers
|
|||
[RequestSizeLimit(50_000_000)]
|
||||
public async Task<ActionResult<ImportResultDto>> ImportExcel([FromForm] ImportExcelForm form)
|
||||
{
|
||||
var tenantId = _tenantProvider.TenantId;
|
||||
if (!tenantId.HasValue || tenantId.Value == Guid.Empty)
|
||||
return Unauthorized("Tenant inválido.");
|
||||
|
||||
var file = form.File;
|
||||
if (file == null || file.Length == 0) return BadRequest("Arquivo inválido.");
|
||||
|
||||
await using var tx = await _db.Database.BeginTransactionAsync();
|
||||
SpreadsheetImportAuditSession? auditSession = null;
|
||||
|
||||
try
|
||||
{
|
||||
|
|
@ -632,6 +648,7 @@ namespace line_gestao_api.Controllers
|
|||
|
||||
var buffer = new List<MobileLine>(600);
|
||||
var imported = 0;
|
||||
var maxItemFromGeral = 0;
|
||||
|
||||
var lastRow = ws.LastRowUsed()?.RowNumber() ?? startRow;
|
||||
|
||||
|
|
@ -639,6 +656,7 @@ namespace line_gestao_api.Controllers
|
|||
{
|
||||
var itemStr = GetCellString(ws, r, colItem);
|
||||
if (string.IsNullOrWhiteSpace(itemStr)) break;
|
||||
var item = TryInt(itemStr);
|
||||
|
||||
var linhaDigits = OnlyDigits(GetCellByHeader(ws, r, map, "LINHA"));
|
||||
var chipDigits = OnlyDigits(GetCellByHeader(ws, r, map, "CHIP"));
|
||||
|
|
@ -652,7 +670,7 @@ namespace line_gestao_api.Controllers
|
|||
var e = new MobileLine
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Item = TryInt(itemStr),
|
||||
Item = item,
|
||||
Conta = GetCellByHeader(ws, r, map, "CONTA"),
|
||||
Linha = linhaVal,
|
||||
Chip = chipVal,
|
||||
|
|
@ -693,6 +711,7 @@ namespace line_gestao_api.Controllers
|
|||
|
||||
buffer.Add(e);
|
||||
imported++;
|
||||
if (item > maxItemFromGeral) maxItemFromGeral = item;
|
||||
|
||||
if (buffer.Count >= 500)
|
||||
{
|
||||
|
|
@ -708,6 +727,11 @@ namespace line_gestao_api.Controllers
|
|||
await _db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
auditSession = _spreadsheetImportAuditService.StartRun(
|
||||
file.FileName,
|
||||
maxItemFromGeral,
|
||||
imported);
|
||||
|
||||
// =========================
|
||||
// ✅ IMPORTA MUREG (ALTERADO: NÃO ESTOURA ERRO SE LINHANOVA JÁ EXISTIR)
|
||||
// =========================
|
||||
|
|
@ -746,13 +770,23 @@ namespace line_gestao_api.Controllers
|
|||
// =========================
|
||||
// ✅ IMPORTA RESUMO
|
||||
// =========================
|
||||
await ImportResumoFromWorkbook(wb);
|
||||
if (auditSession == null)
|
||||
{
|
||||
throw new InvalidOperationException("Sessão de auditoria não iniciada.");
|
||||
}
|
||||
|
||||
await ImportResumoFromWorkbook(wb, auditSession);
|
||||
|
||||
// =========================
|
||||
// ✅ IMPORTA PARCELAMENTOS
|
||||
// =========================
|
||||
var parcelamentosSummary = await _parcelamentosImportService.ImportFromWorkbookAsync(wb, replaceAll: true);
|
||||
if (auditSession != null)
|
||||
{
|
||||
await _spreadsheetImportAuditService.SaveRunAsync(auditSession);
|
||||
}
|
||||
|
||||
await AddSpreadsheetImportHistoryAsync(file.FileName, imported, parcelamentosSummary);
|
||||
await tx.CommitAsync();
|
||||
return Ok(new ImportResultDto { Imported = imported, Parcelamentos = parcelamentosSummary });
|
||||
}
|
||||
|
|
@ -763,6 +797,57 @@ namespace line_gestao_api.Controllers
|
|||
}
|
||||
}
|
||||
|
||||
private async Task AddSpreadsheetImportHistoryAsync(
|
||||
string? fileName,
|
||||
int imported,
|
||||
ParcelamentosImportSummaryDto? parcelamentosSummary)
|
||||
{
|
||||
var tenantId = _tenantProvider.TenantId;
|
||||
if (!tenantId.HasValue)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var claimNameId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
||||
var userId = Guid.TryParse(claimNameId, out var parsedUserId) ? parsedUserId : (Guid?)null;
|
||||
|
||||
var userName = User.FindFirst("name")?.Value
|
||||
?? User.FindFirst(ClaimTypes.Name)?.Value
|
||||
?? User.Identity?.Name;
|
||||
|
||||
var userEmail = User.FindFirst(ClaimTypes.Email)?.Value
|
||||
?? User.FindFirst("email")?.Value;
|
||||
|
||||
var changes = new List<AuditFieldChangeDto>
|
||||
{
|
||||
new() { Field = "Arquivo", ChangeType = "imported", NewValue = string.IsNullOrWhiteSpace(fileName) ? "-" : fileName },
|
||||
new() { Field = "LinhasImportadasGeral", ChangeType = "imported", NewValue = imported.ToString(CultureInfo.InvariantCulture) },
|
||||
new() { Field = "ParcelamentosLidos", ChangeType = "imported", NewValue = (parcelamentosSummary?.Lidos ?? 0).ToString(CultureInfo.InvariantCulture) },
|
||||
new() { Field = "ParcelamentosInseridos", ChangeType = "imported", NewValue = (parcelamentosSummary?.Inseridos ?? 0).ToString(CultureInfo.InvariantCulture) },
|
||||
new() { Field = "ParcelamentosAtualizados", ChangeType = "imported", NewValue = (parcelamentosSummary?.Atualizados ?? 0).ToString(CultureInfo.InvariantCulture) }
|
||||
};
|
||||
|
||||
_db.AuditLogs.Add(new AuditLog
|
||||
{
|
||||
TenantId = tenantId.Value,
|
||||
OccurredAtUtc = DateTime.UtcNow,
|
||||
UserId = userId,
|
||||
UserName = string.IsNullOrWhiteSpace(userName) ? "USUARIO" : userName,
|
||||
UserEmail = userEmail,
|
||||
Action = "IMPORT",
|
||||
Page = AuditLogBuilder.SpreadsheetImportPageName,
|
||||
EntityName = "SpreadsheetImport",
|
||||
EntityId = null,
|
||||
EntityLabel = "Importação Excel",
|
||||
ChangesJson = JsonSerializer.Serialize(changes),
|
||||
RequestPath = HttpContext.Request.Path.Value,
|
||||
RequestMethod = HttpContext.Request.Method,
|
||||
IpAddress = HttpContext.Connection.RemoteIpAddress?.ToString()
|
||||
});
|
||||
|
||||
await _db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
// ==========================================================
|
||||
// ✅ IMPORTAÇÃO DA ABA MUREG
|
||||
// ✅ NOVA REGRA:
|
||||
|
|
@ -1620,7 +1705,7 @@ namespace line_gestao_api.Controllers
|
|||
// ==========================================================
|
||||
// ✅ IMPORTAÇÃO DA ABA RESUMO
|
||||
// ==========================================================
|
||||
private async Task ImportResumoFromWorkbook(XLWorkbook wb)
|
||||
private async Task ImportResumoFromWorkbook(XLWorkbook wb, SpreadsheetImportAuditSession auditSession)
|
||||
{
|
||||
var ws = wb.Worksheets.FirstOrDefault(w => NormalizeHeader(w.Name) == NormalizeHeader("RESUMO"));
|
||||
if (ws == null) return;
|
||||
|
|
@ -1638,15 +1723,15 @@ namespace line_gestao_api.Controllers
|
|||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
await ImportResumoTabela1(ws, now);
|
||||
await ImportResumoTabela2(ws, now);
|
||||
await ImportResumoTabela1(ws, now, auditSession);
|
||||
await ImportResumoTabela2(ws, now, auditSession);
|
||||
await ImportResumoTabela3(ws, now);
|
||||
await ImportResumoTabela4(ws, now);
|
||||
await ImportResumoTabela5(ws, now);
|
||||
await ImportResumoTabela6(ws, now);
|
||||
}
|
||||
|
||||
private async Task ImportResumoTabela1(IXLWorksheet ws, DateTime now)
|
||||
private async Task ImportResumoTabela1(IXLWorksheet ws, DateTime now, SpreadsheetImportAuditSession auditSession)
|
||||
{
|
||||
var lastRowUsed = ws.LastRowUsed()?.RowNumber() ?? 1;
|
||||
var headerRow = FindHeaderRowForMacrophonyPlans(ws, 1, lastRowUsed);
|
||||
|
|
@ -1785,10 +1870,16 @@ namespace line_gestao_api.Controllers
|
|||
return;
|
||||
}
|
||||
|
||||
var totalLinhasSource = TryNullableInt(GetCellString(ws, totalRowIndex.Value, colTotalLinhas));
|
||||
var total = new ResumoMacrophonyTotal
|
||||
{
|
||||
FranquiaGbTotal = TryDecimal(GetCellString(ws, totalRowIndex.Value, colFranquiaGb)),
|
||||
TotalLinhasTotal = TryNullableInt(GetCellString(ws, totalRowIndex.Value, colTotalLinhas)),
|
||||
TotalLinhasTotal = _spreadsheetImportAuditService.CanonicalizeTotalLinhas(
|
||||
auditSession,
|
||||
"RESUMO.MACROPHONY_TOTAL",
|
||||
"TotalLinhasTotal",
|
||||
totalLinhasSource,
|
||||
"HIGH"),
|
||||
ValorTotal = TryDecimal(GetCellString(ws, totalRowIndex.Value, colValorTotal)),
|
||||
CreatedAt = now,
|
||||
UpdatedAt = now
|
||||
|
|
@ -1819,7 +1910,7 @@ namespace line_gestao_api.Controllers
|
|||
return 0;
|
||||
}
|
||||
|
||||
private async Task ImportResumoTabela2(IXLWorksheet ws, DateTime now)
|
||||
private async Task ImportResumoTabela2(IXLWorksheet ws, DateTime now, SpreadsheetImportAuditSession auditSession)
|
||||
{
|
||||
const int headerRow = 5;
|
||||
const int totalRow = 219;
|
||||
|
|
@ -1881,9 +1972,15 @@ namespace line_gestao_api.Controllers
|
|||
await _db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
var qtdLinhasTotalSource = TryNullableInt(GetCellString(ws, totalRow, colQtdLinhas));
|
||||
var total = new ResumoVivoLineTotal
|
||||
{
|
||||
QtdLinhasTotal = TryNullableInt(GetCellString(ws, totalRow, colQtdLinhas)),
|
||||
QtdLinhasTotal = _spreadsheetImportAuditService.CanonicalizeTotalLinhas(
|
||||
auditSession,
|
||||
"RESUMO.VIVO_LINE_TOTAL",
|
||||
"QtdLinhasTotal",
|
||||
qtdLinhasTotalSource,
|
||||
"HIGH"),
|
||||
FranquiaTotal = TryDecimal(GetCellString(ws, totalRow, colFranquiaTotal)),
|
||||
ValorContratoVivo = TryDecimal(GetCellString(ws, totalRow, colValorContratoVivo)),
|
||||
FranquiaLine = TryDecimal(GetCellString(ws, totalRow, colFranquiaLine)),
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
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;
|
||||
|
||||
|
|
@ -8,6 +9,7 @@ namespace line_gestao_api.Controllers
|
|||
{
|
||||
[ApiController]
|
||||
[Route("api/mureg")]
|
||||
[Authorize]
|
||||
public class MuregController : ControllerBase
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
|
|
@ -345,6 +347,7 @@ namespace line_gestao_api.Controllers
|
|||
// Exclui registro MUREG
|
||||
// ==========================================================
|
||||
[HttpDelete("{id:guid}")]
|
||||
[Authorize(Roles = "admin")]
|
||||
public async Task<IActionResult> Delete(Guid id)
|
||||
{
|
||||
var entity = await _db.MuregLines.FirstOrDefaultAsync(x => x.Id == id);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using line_gestao_api.Data;
|
||||
using line_gestao_api.Dtos;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
|
|
@ -7,6 +8,7 @@ namespace line_gestao_api.Controllers
|
|||
{
|
||||
[ApiController]
|
||||
[Route("api/relatorios")]
|
||||
[Authorize]
|
||||
public class RelatoriosController : ControllerBase
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using line_gestao_api.Data;
|
||||
using line_gestao_api.Dtos;
|
||||
using line_gestao_api.Services;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
|
@ -12,10 +13,12 @@ namespace line_gestao_api.Controllers;
|
|||
public class ResumoController : ControllerBase
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
private readonly SpreadsheetImportAuditService _spreadsheetImportAuditService;
|
||||
|
||||
public ResumoController(AppDbContext db)
|
||||
public ResumoController(AppDbContext db, SpreadsheetImportAuditService spreadsheetImportAuditService)
|
||||
{
|
||||
_db = db;
|
||||
_spreadsheetImportAuditService = spreadsheetImportAuditService;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
|
|
@ -46,6 +49,7 @@ public class ResumoController : ControllerBase
|
|||
|
||||
var reservaTotalEntity = await _db.ResumoReservaTotals.AsNoTracking()
|
||||
.FirstOrDefaultAsync();
|
||||
var canonicalTotalLinhas = await _spreadsheetImportAuditService.GetCanonicalTotalLinhasForReadAsync();
|
||||
|
||||
var response = new ResumoResponseDto
|
||||
{
|
||||
|
|
@ -66,7 +70,7 @@ public class ResumoController : ControllerBase
|
|||
.Select(x => new ResumoMacrophonyTotalDto
|
||||
{
|
||||
FranquiaGbTotal = x.FranquiaGbTotal,
|
||||
TotalLinhasTotal = x.TotalLinhasTotal,
|
||||
TotalLinhasTotal = canonicalTotalLinhas,
|
||||
ValorTotal = x.ValorTotal
|
||||
})
|
||||
.FirstOrDefaultAsync(),
|
||||
|
|
@ -87,7 +91,7 @@ public class ResumoController : ControllerBase
|
|||
VivoLineTotals = await _db.ResumoVivoLineTotals.AsNoTracking()
|
||||
.Select(x => new ResumoVivoLineTotalDto
|
||||
{
|
||||
QtdLinhasTotal = x.QtdLinhasTotal,
|
||||
QtdLinhasTotal = canonicalTotalLinhas,
|
||||
FranquiaTotal = x.FranquiaTotal,
|
||||
ValorContratoVivo = x.ValorContratoVivo,
|
||||
FranquiaLine = x.FranquiaLine,
|
||||
|
|
@ -151,6 +155,12 @@ public class ResumoController : ControllerBase
|
|||
}
|
||||
};
|
||||
|
||||
response.MacrophonyTotals ??= new ResumoMacrophonyTotalDto();
|
||||
response.MacrophonyTotals.TotalLinhasTotal = canonicalTotalLinhas;
|
||||
|
||||
response.VivoLineTotals ??= new ResumoVivoLineTotalDto();
|
||||
response.VivoLineTotals.QtdLinhasTotal = canonicalTotalLinhas;
|
||||
|
||||
return Ok(response);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
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;
|
||||
using System.Globalization;
|
||||
|
|
@ -10,6 +11,7 @@ namespace line_gestao_api.Controllers
|
|||
{
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
[Authorize]
|
||||
public class TrocaNumeroController : ControllerBase
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
|
|
@ -156,6 +158,7 @@ namespace line_gestao_api.Controllers
|
|||
// ✅ DELETE
|
||||
// ==========================================================
|
||||
[HttpDelete("{id:guid}")]
|
||||
[Authorize(Roles = "admin")]
|
||||
public async Task<IActionResult> Delete(Guid id)
|
||||
{
|
||||
var x = await _db.TrocaNumeroLines.FirstOrDefaultAsync(a => a.Id == id);
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ namespace line_gestao_api.Controllers
|
|||
{
|
||||
[ApiController]
|
||||
[Route("api/user-data")]
|
||||
[Authorize]
|
||||
public class UserDataController : ControllerBase
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ namespace line_gestao_api.Controllers
|
|||
{
|
||||
[ApiController]
|
||||
[Route("api/lines/vigencia")]
|
||||
[Authorize]
|
||||
public class VigenciaController : ControllerBase
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
|
|
|
|||
|
|
@ -68,6 +68,10 @@ public class AppDbContext : IdentityDbContext<ApplicationUser, IdentityRole<Guid
|
|||
// ✅ tabela AUDIT LOGS (Histórico)
|
||||
public DbSet<AuditLog> AuditLogs => Set<AuditLog>();
|
||||
|
||||
// ✅ tabelas de auditoria de importação
|
||||
public DbSet<ImportAuditRun> ImportAuditRuns => Set<ImportAuditRun>();
|
||||
public DbSet<ImportAuditIssue> ImportAuditIssues => Set<ImportAuditIssue>();
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
base.OnModelCreating(modelBuilder);
|
||||
|
|
@ -283,6 +287,35 @@ public class AppDbContext : IdentityDbContext<ApplicationUser, IdentityRole<Guid
|
|||
e.HasIndex(x => x.EntityName);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<ImportAuditRun>(e =>
|
||||
{
|
||||
e.Property(x => x.FileName).HasMaxLength(260);
|
||||
e.Property(x => x.Status).HasMaxLength(40);
|
||||
|
||||
e.HasIndex(x => x.TenantId);
|
||||
e.HasIndex(x => x.ImportedAt);
|
||||
e.HasIndex(x => x.Status);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<ImportAuditIssue>(e =>
|
||||
{
|
||||
e.Property(x => x.Entity).HasMaxLength(120);
|
||||
e.Property(x => x.FieldName).HasMaxLength(120);
|
||||
e.Property(x => x.SourceValue).HasMaxLength(120);
|
||||
e.Property(x => x.CanonicalValue).HasMaxLength(120);
|
||||
e.Property(x => x.Resolution).HasMaxLength(80);
|
||||
e.Property(x => x.Severity).HasMaxLength(40);
|
||||
|
||||
e.HasIndex(x => x.TenantId);
|
||||
e.HasIndex(x => x.AuditRunId);
|
||||
e.HasIndex(x => x.Entity);
|
||||
|
||||
e.HasOne(x => x.AuditRun)
|
||||
.WithMany(x => x.Issues)
|
||||
.HasForeignKey(x => x.AuditRunId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<MobileLine>().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId);
|
||||
modelBuilder.Entity<MuregLine>().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId);
|
||||
modelBuilder.Entity<BillingClient>().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId);
|
||||
|
|
@ -305,6 +338,8 @@ public class AppDbContext : IdentityDbContext<ApplicationUser, IdentityRole<Guid
|
|||
modelBuilder.Entity<ParcelamentoLine>().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId);
|
||||
modelBuilder.Entity<ParcelamentoMonthValue>().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId);
|
||||
modelBuilder.Entity<AuditLog>().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId);
|
||||
modelBuilder.Entity<ImportAuditRun>().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId);
|
||||
modelBuilder.Entity<ImportAuditIssue>().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId);
|
||||
modelBuilder.Entity<ApplicationUser>().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId);
|
||||
}
|
||||
|
||||
|
|
|
|||
133
Data/SeedData.cs
133
Data/SeedData.cs
|
|
@ -52,6 +52,8 @@ public static class SeedData
|
|||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
await NormalizeLegacyTenantDataAsync(db, tenant.Id);
|
||||
|
||||
tenantProvider.SetTenantId(tenant.Id);
|
||||
|
||||
var normalizedEmail = userManager.NormalizeEmail(options.AdminEmail);
|
||||
|
|
@ -79,4 +81,135 @@ public static class SeedData
|
|||
|
||||
tenantProvider.SetTenantId(null);
|
||||
}
|
||||
|
||||
private static async Task NormalizeLegacyTenantDataAsync(AppDbContext db, Guid defaultTenantId)
|
||||
{
|
||||
if (defaultTenantId == Guid.Empty)
|
||||
return;
|
||||
|
||||
await db.Users
|
||||
.IgnoreQueryFilters()
|
||||
.Where(x => x.TenantId == Guid.Empty)
|
||||
.ExecuteUpdateAsync(setters => setters.SetProperty(x => x.TenantId, defaultTenantId));
|
||||
|
||||
await db.MobileLines
|
||||
.IgnoreQueryFilters()
|
||||
.Where(x => x.TenantId == Guid.Empty)
|
||||
.ExecuteUpdateAsync(setters => setters.SetProperty(x => x.TenantId, defaultTenantId));
|
||||
|
||||
await db.MuregLines
|
||||
.IgnoreQueryFilters()
|
||||
.Where(x => x.TenantId == Guid.Empty)
|
||||
.ExecuteUpdateAsync(setters => setters.SetProperty(x => x.TenantId, defaultTenantId));
|
||||
|
||||
await db.BillingClients
|
||||
.IgnoreQueryFilters()
|
||||
.Where(x => x.TenantId == Guid.Empty)
|
||||
.ExecuteUpdateAsync(setters => setters.SetProperty(x => x.TenantId, defaultTenantId));
|
||||
|
||||
await db.UserDatas
|
||||
.IgnoreQueryFilters()
|
||||
.Where(x => x.TenantId == Guid.Empty)
|
||||
.ExecuteUpdateAsync(setters => setters.SetProperty(x => x.TenantId, defaultTenantId));
|
||||
|
||||
await db.VigenciaLines
|
||||
.IgnoreQueryFilters()
|
||||
.Where(x => x.TenantId == Guid.Empty)
|
||||
.ExecuteUpdateAsync(setters => setters.SetProperty(x => x.TenantId, defaultTenantId));
|
||||
|
||||
await db.TrocaNumeroLines
|
||||
.IgnoreQueryFilters()
|
||||
.Where(x => x.TenantId == Guid.Empty)
|
||||
.ExecuteUpdateAsync(setters => setters.SetProperty(x => x.TenantId, defaultTenantId));
|
||||
|
||||
await db.ChipVirgemLines
|
||||
.IgnoreQueryFilters()
|
||||
.Where(x => x.TenantId == Guid.Empty)
|
||||
.ExecuteUpdateAsync(setters => setters.SetProperty(x => x.TenantId, defaultTenantId));
|
||||
|
||||
await db.ControleRecebidoLines
|
||||
.IgnoreQueryFilters()
|
||||
.Where(x => x.TenantId == Guid.Empty)
|
||||
.ExecuteUpdateAsync(setters => setters.SetProperty(x => x.TenantId, defaultTenantId));
|
||||
|
||||
await db.Notifications
|
||||
.IgnoreQueryFilters()
|
||||
.Where(x => x.TenantId == Guid.Empty)
|
||||
.ExecuteUpdateAsync(setters => setters.SetProperty(x => x.TenantId, defaultTenantId));
|
||||
|
||||
await db.ResumoMacrophonyPlans
|
||||
.IgnoreQueryFilters()
|
||||
.Where(x => x.TenantId == Guid.Empty)
|
||||
.ExecuteUpdateAsync(setters => setters.SetProperty(x => x.TenantId, defaultTenantId));
|
||||
|
||||
await db.ResumoMacrophonyTotals
|
||||
.IgnoreQueryFilters()
|
||||
.Where(x => x.TenantId == Guid.Empty)
|
||||
.ExecuteUpdateAsync(setters => setters.SetProperty(x => x.TenantId, defaultTenantId));
|
||||
|
||||
await db.ResumoVivoLineResumos
|
||||
.IgnoreQueryFilters()
|
||||
.Where(x => x.TenantId == Guid.Empty)
|
||||
.ExecuteUpdateAsync(setters => setters.SetProperty(x => x.TenantId, defaultTenantId));
|
||||
|
||||
await db.ResumoVivoLineTotals
|
||||
.IgnoreQueryFilters()
|
||||
.Where(x => x.TenantId == Guid.Empty)
|
||||
.ExecuteUpdateAsync(setters => setters.SetProperty(x => x.TenantId, defaultTenantId));
|
||||
|
||||
await db.ResumoClienteEspeciais
|
||||
.IgnoreQueryFilters()
|
||||
.Where(x => x.TenantId == Guid.Empty)
|
||||
.ExecuteUpdateAsync(setters => setters.SetProperty(x => x.TenantId, defaultTenantId));
|
||||
|
||||
await db.ResumoPlanoContratoResumos
|
||||
.IgnoreQueryFilters()
|
||||
.Where(x => x.TenantId == Guid.Empty)
|
||||
.ExecuteUpdateAsync(setters => setters.SetProperty(x => x.TenantId, defaultTenantId));
|
||||
|
||||
await db.ResumoPlanoContratoTotals
|
||||
.IgnoreQueryFilters()
|
||||
.Where(x => x.TenantId == Guid.Empty)
|
||||
.ExecuteUpdateAsync(setters => setters.SetProperty(x => x.TenantId, defaultTenantId));
|
||||
|
||||
await db.ResumoLineTotais
|
||||
.IgnoreQueryFilters()
|
||||
.Where(x => x.TenantId == Guid.Empty)
|
||||
.ExecuteUpdateAsync(setters => setters.SetProperty(x => x.TenantId, defaultTenantId));
|
||||
|
||||
await db.ResumoReservaLines
|
||||
.IgnoreQueryFilters()
|
||||
.Where(x => x.TenantId == Guid.Empty)
|
||||
.ExecuteUpdateAsync(setters => setters.SetProperty(x => x.TenantId, defaultTenantId));
|
||||
|
||||
await db.ResumoReservaTotals
|
||||
.IgnoreQueryFilters()
|
||||
.Where(x => x.TenantId == Guid.Empty)
|
||||
.ExecuteUpdateAsync(setters => setters.SetProperty(x => x.TenantId, defaultTenantId));
|
||||
|
||||
await db.ParcelamentoLines
|
||||
.IgnoreQueryFilters()
|
||||
.Where(x => x.TenantId == Guid.Empty)
|
||||
.ExecuteUpdateAsync(setters => setters.SetProperty(x => x.TenantId, defaultTenantId));
|
||||
|
||||
await db.ParcelamentoMonthValues
|
||||
.IgnoreQueryFilters()
|
||||
.Where(x => x.TenantId == Guid.Empty)
|
||||
.ExecuteUpdateAsync(setters => setters.SetProperty(x => x.TenantId, defaultTenantId));
|
||||
|
||||
await db.AuditLogs
|
||||
.IgnoreQueryFilters()
|
||||
.Where(x => x.TenantId == Guid.Empty)
|
||||
.ExecuteUpdateAsync(setters => setters.SetProperty(x => x.TenantId, defaultTenantId));
|
||||
|
||||
await db.ImportAuditRuns
|
||||
.IgnoreQueryFilters()
|
||||
.Where(x => x.TenantId == Guid.Empty)
|
||||
.ExecuteUpdateAsync(setters => setters.SetProperty(x => x.TenantId, defaultTenantId));
|
||||
|
||||
await db.ImportAuditIssues
|
||||
.IgnoreQueryFilters()
|
||||
.Where(x => x.TenantId == Guid.Empty)
|
||||
.ExecuteUpdateAsync(setters => setters.SetProperty(x => x.TenantId, defaultTenantId));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
namespace line_gestao_api.Dtos;
|
||||
|
||||
public sealed class ImportAuditRunDto
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public DateTime ImportedAt { get; set; }
|
||||
public string? FileName { get; set; }
|
||||
public string Status { get; set; } = string.Empty;
|
||||
public int CanonicalTotalLinhas { get; set; }
|
||||
public int SourceMaxItemGeral { get; set; }
|
||||
public int SourceValidCountGeral { get; set; }
|
||||
public int IssueCount { get; set; }
|
||||
public List<ImportAuditIssueDto> Issues { get; set; } = new();
|
||||
}
|
||||
|
||||
public sealed class ImportAuditIssueDto
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public string Entity { get; set; } = string.Empty;
|
||||
public string FieldName { get; set; } = string.Empty;
|
||||
public string? SourceValue { get; set; }
|
||||
public string CanonicalValue { get; set; } = string.Empty;
|
||||
public string Resolution { get; set; } = string.Empty;
|
||||
public string Severity { get; set; } = string.Empty;
|
||||
public DateTime CreatedAt { get; set; }
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
// <auto-generated />
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using line_gestao_api.Data;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace line_gestao_api.Migrations
|
||||
{
|
||||
[DbContext(typeof(AppDbContext))]
|
||||
[Migration("20260213120000_AddImportAuditTables")]
|
||||
partial class AddImportAuditTables
|
||||
{
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace line_gestao_api.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddImportAuditTables : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ImportAuditRuns",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
TenantId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
ImportedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
FileName = table.Column<string>(type: "character varying(260)", maxLength: 260, nullable: true),
|
||||
Status = table.Column<string>(type: "character varying(40)", maxLength: 40, nullable: false),
|
||||
CanonicalTotalLinhas = table.Column<int>(type: "integer", nullable: false),
|
||||
SourceMaxItemGeral = table.Column<int>(type: "integer", nullable: false),
|
||||
SourceValidCountGeral = table.Column<int>(type: "integer", nullable: false),
|
||||
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ImportAuditRuns", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ImportAuditIssues",
|
||||
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),
|
||||
Entity = table.Column<string>(type: "character varying(120)", maxLength: 120, nullable: false),
|
||||
FieldName = table.Column<string>(type: "character varying(120)", maxLength: 120, nullable: false),
|
||||
SourceValue = table.Column<string>(type: "character varying(120)", maxLength: 120, nullable: true),
|
||||
CanonicalValue = table.Column<string>(type: "character varying(120)", maxLength: 120, nullable: false),
|
||||
Resolution = table.Column<string>(type: "character varying(80)", maxLength: 80, nullable: false),
|
||||
Severity = table.Column<string>(type: "character varying(40)", maxLength: 40, nullable: false),
|
||||
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ImportAuditIssues", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_ImportAuditIssues_ImportAuditRuns_AuditRunId",
|
||||
column: x => x.AuditRunId,
|
||||
principalTable: "ImportAuditRuns",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ImportAuditIssues_AuditRunId",
|
||||
table: "ImportAuditIssues",
|
||||
column: "AuditRunId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ImportAuditIssues_Entity",
|
||||
table: "ImportAuditIssues",
|
||||
column: "Entity");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ImportAuditIssues_TenantId",
|
||||
table: "ImportAuditIssues",
|
||||
column: "TenantId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ImportAuditRuns_ImportedAt",
|
||||
table: "ImportAuditRuns",
|
||||
column: "ImportedAt");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ImportAuditRuns_Status",
|
||||
table: "ImportAuditRuns",
|
||||
column: "Status");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ImportAuditRuns_TenantId",
|
||||
table: "ImportAuditRuns",
|
||||
column: "TenantId");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "ImportAuditIssues");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "ImportAuditRuns");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
// <auto-generated />
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using line_gestao_api.Data;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace line_gestao_api.Migrations
|
||||
{
|
||||
[DbContext(typeof(AppDbContext))]
|
||||
[Migration("20260213130000_RemoveDetailedImportLogs")]
|
||||
partial class RemoveDetailedImportLogs
|
||||
{
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace line_gestao_api.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class RemoveDetailedImportLogs : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.Sql(
|
||||
"""
|
||||
DELETE FROM "AuditLogs"
|
||||
WHERE "RequestPath" ILIKE '%import-excel%'
|
||||
AND COALESCE("Page", '') <> 'Importação de Planilha';
|
||||
""");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
// Nao ha restauracao segura para os logs removidos.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -497,6 +497,108 @@ namespace line_gestao_api.Migrations
|
|||
b.ToTable("ControleRecebidoLines");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("line_gestao_api.Models.ImportAuditIssue", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("AuditRunId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("CanonicalValue")
|
||||
.IsRequired()
|
||||
.HasMaxLength(120)
|
||||
.HasColumnType("character varying(120)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Entity")
|
||||
.IsRequired()
|
||||
.HasMaxLength(120)
|
||||
.HasColumnType("character varying(120)");
|
||||
|
||||
b.Property<string>("FieldName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(120)
|
||||
.HasColumnType("character varying(120)");
|
||||
|
||||
b.Property<string>("Resolution")
|
||||
.IsRequired()
|
||||
.HasMaxLength(80)
|
||||
.HasColumnType("character varying(80)");
|
||||
|
||||
b.Property<string>("Severity")
|
||||
.IsRequired()
|
||||
.HasMaxLength(40)
|
||||
.HasColumnType("character varying(40)");
|
||||
|
||||
b.Property<string>("SourceValue")
|
||||
.HasMaxLength(120)
|
||||
.HasColumnType("character varying(120)");
|
||||
|
||||
b.Property<Guid>("TenantId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AuditRunId");
|
||||
|
||||
b.HasIndex("Entity");
|
||||
|
||||
b.HasIndex("TenantId");
|
||||
|
||||
b.ToTable("ImportAuditIssues");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("line_gestao_api.Models.ImportAuditRun", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<int>("CanonicalTotalLinhas")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("FileName")
|
||||
.HasMaxLength(260)
|
||||
.HasColumnType("character varying(260)");
|
||||
|
||||
b.Property<DateTime>("ImportedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<int>("SourceMaxItemGeral")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("SourceValidCountGeral")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasMaxLength(40)
|
||||
.HasColumnType("character varying(40)");
|
||||
|
||||
b.Property<Guid>("TenantId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ImportedAt");
|
||||
|
||||
b.HasIndex("Status");
|
||||
|
||||
b.HasIndex("TenantId");
|
||||
|
||||
b.ToTable("ImportAuditRuns");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("line_gestao_api.Models.MobileLine", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
|
|
@ -1466,6 +1568,17 @@ namespace line_gestao_api.Migrations
|
|||
b.Navigation("MobileLine");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("line_gestao_api.Models.ImportAuditIssue", b =>
|
||||
{
|
||||
b.HasOne("line_gestao_api.Models.ImportAuditRun", "AuditRun")
|
||||
.WithMany("Issues")
|
||||
.HasForeignKey("AuditRunId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("AuditRun");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("line_gestao_api.Models.Notification", b =>
|
||||
{
|
||||
b.HasOne("line_gestao_api.Models.ApplicationUser", "User")
|
||||
|
|
@ -1499,6 +1612,11 @@ namespace line_gestao_api.Migrations
|
|||
b.Navigation("Muregs");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("line_gestao_api.Models.ImportAuditRun", b =>
|
||||
{
|
||||
b.Navigation("Issues");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("line_gestao_api.Models.ParcelamentoLine", b =>
|
||||
{
|
||||
b.Navigation("MonthValues");
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
namespace line_gestao_api.Models;
|
||||
|
||||
public class ImportAuditIssue : ITenantEntity
|
||||
{
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
|
||||
public Guid TenantId { get; set; }
|
||||
|
||||
public Guid AuditRunId { get; set; }
|
||||
public ImportAuditRun AuditRun { get; set; } = default!;
|
||||
|
||||
public string Entity { get; set; } = string.Empty;
|
||||
|
||||
public string FieldName { get; set; } = string.Empty;
|
||||
|
||||
public string? SourceValue { get; set; }
|
||||
|
||||
public string CanonicalValue { get; set; } = string.Empty;
|
||||
|
||||
public string Resolution { get; set; } = "OVERRIDDEN_BY_GERAL";
|
||||
|
||||
public string Severity { get; set; } = "WARNING";
|
||||
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
namespace line_gestao_api.Models;
|
||||
|
||||
public class ImportAuditRun : ITenantEntity
|
||||
{
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
|
||||
public Guid TenantId { get; set; }
|
||||
|
||||
public DateTime ImportedAt { get; set; } = DateTime.UtcNow;
|
||||
|
||||
public string? FileName { get; set; }
|
||||
|
||||
public string Status { get; set; } = "SUCCESS";
|
||||
|
||||
public int CanonicalTotalLinhas { get; set; }
|
||||
|
||||
public int SourceMaxItemGeral { get; set; }
|
||||
|
||||
public int SourceValidCountGeral { get; set; }
|
||||
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
|
||||
|
||||
public ICollection<ImportAuditIssue> Issues { get; set; } = new List<ImportAuditIssue>();
|
||||
}
|
||||
|
|
@ -38,6 +38,7 @@ builder.Services.AddScoped<ITenantProvider, TenantProvider>();
|
|||
builder.Services.AddScoped<IAuditLogBuilder, AuditLogBuilder>();
|
||||
builder.Services.AddScoped<ParcelamentosImportService>();
|
||||
builder.Services.AddScoped<GeralDashboardInsightsService>();
|
||||
builder.Services.AddScoped<SpreadsheetImportAuditService>();
|
||||
|
||||
builder.Services.AddIdentityCore<ApplicationUser>(options =>
|
||||
{
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@ namespace line_gestao_api.Services;
|
|||
|
||||
public class AuditLogBuilder : IAuditLogBuilder
|
||||
{
|
||||
public const string SpreadsheetImportPageName = "Importação de Planilha";
|
||||
|
||||
private static readonly Dictionary<string, string> PageByEntity = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
{ nameof(MobileLine), "Geral" },
|
||||
|
|
@ -80,6 +82,12 @@ public class AuditLogBuilder : IAuditLogBuilder
|
|||
var requestMethod = request?.Method;
|
||||
var ipAddress = httpContext?.Connection?.RemoteIpAddress?.ToString();
|
||||
|
||||
if (IsSpreadsheetImportRequest(requestPath))
|
||||
{
|
||||
// Importacoes de planilha nao geram historico detalhado por entidade.
|
||||
return new List<AuditLog>();
|
||||
}
|
||||
|
||||
var logs = new List<AuditLog>();
|
||||
|
||||
foreach (var entry in changeTracker.Entries())
|
||||
|
|
@ -120,6 +128,16 @@ public class AuditLogBuilder : IAuditLogBuilder
|
|||
return logs;
|
||||
}
|
||||
|
||||
private static bool IsSpreadsheetImportRequest(string? requestPath)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(requestPath))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return requestPath.Contains("/import-excel", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static string ResolveAction(EntityState state)
|
||||
=> state switch
|
||||
{
|
||||
|
|
|
|||
|
|
@ -523,9 +523,44 @@ namespace line_gestao_api.Services
|
|||
ClientGroups = BuildClientGroups(clientGroupsRaw)
|
||||
};
|
||||
|
||||
dto.Kpis.TotalLinhas = await ResolveCanonicalTotalLinhasAsync(dto.Kpis.TotalLinhas);
|
||||
|
||||
return dto;
|
||||
}
|
||||
|
||||
private async Task<int> ResolveCanonicalTotalLinhasAsync(int fallback)
|
||||
{
|
||||
try
|
||||
{
|
||||
var fromLatestRun = await _db.ImportAuditRuns
|
||||
.AsNoTracking()
|
||||
.OrderByDescending(x => x.ImportedAt)
|
||||
.ThenByDescending(x => x.Id)
|
||||
.Select(x => (int?)x.CanonicalTotalLinhas)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
if (fromLatestRun.HasValue && fromLatestRun.Value > 0)
|
||||
{
|
||||
return fromLatestRun.Value;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Fallback para ambientes em que a migration ainda não foi aplicada.
|
||||
}
|
||||
|
||||
var fromMaxItem = await _db.MobileLines
|
||||
.AsNoTracking()
|
||||
.MaxAsync(x => (int?)x.Item);
|
||||
|
||||
if (fromMaxItem.HasValue && fromMaxItem.Value > 0)
|
||||
{
|
||||
return fromMaxItem.Value;
|
||||
}
|
||||
|
||||
return fallback;
|
||||
}
|
||||
|
||||
private static GeralDashboardKpisDto BuildKpis(TotalsProjection? totals)
|
||||
{
|
||||
if (totals == null)
|
||||
|
|
|
|||
|
|
@ -275,7 +275,7 @@ public sealed class ParcelamentosImportService
|
|||
var block1 = ExtractMonthBlock(monthHeaders, firstJanIdx);
|
||||
var block2 = block1.NextIndex < monthHeaders.Count
|
||||
? ExtractMonthBlock(monthHeaders, block1.NextIndex)
|
||||
: (new List<(int Col, int Month)>(), block1.NextIndex);
|
||||
: (Block: new List<(int Col, int Month)>(), NextIndex: block1.NextIndex);
|
||||
|
||||
var yearRow = headerRow - 1;
|
||||
var year1 = block1.Block.Count > 0
|
||||
|
|
|
|||
|
|
@ -0,0 +1,142 @@
|
|||
using System.Globalization;
|
||||
using line_gestao_api.Data;
|
||||
using line_gestao_api.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace line_gestao_api.Services;
|
||||
|
||||
public sealed class SpreadsheetImportAuditService
|
||||
{
|
||||
private const string DefaultResolution = "OVERRIDDEN_BY_GERAL";
|
||||
|
||||
private readonly AppDbContext _db;
|
||||
|
||||
public SpreadsheetImportAuditService(AppDbContext db)
|
||||
{
|
||||
_db = db;
|
||||
}
|
||||
|
||||
public SpreadsheetImportAuditSession StartRun(string? fileName, int sourceMaxItemGeral, int sourceValidCountGeral)
|
||||
{
|
||||
var canonicalTotalLinhas = sourceMaxItemGeral > 0 ? sourceMaxItemGeral : sourceValidCountGeral;
|
||||
|
||||
var run = new ImportAuditRun
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
ImportedAt = DateTime.UtcNow,
|
||||
FileName = string.IsNullOrWhiteSpace(fileName) ? null : fileName.Trim(),
|
||||
Status = "RUNNING",
|
||||
CanonicalTotalLinhas = canonicalTotalLinhas,
|
||||
SourceMaxItemGeral = sourceMaxItemGeral,
|
||||
SourceValidCountGeral = sourceValidCountGeral,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
UpdatedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
return new SpreadsheetImportAuditSession(run);
|
||||
}
|
||||
|
||||
public int CanonicalizeTotalLinhas(
|
||||
SpreadsheetImportAuditSession session,
|
||||
string entity,
|
||||
string fieldName,
|
||||
int? sourceValue,
|
||||
string severity = "WARNING")
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(session);
|
||||
|
||||
var canonicalValue = session.Run.CanonicalTotalLinhas;
|
||||
|
||||
if (sourceValue != canonicalValue)
|
||||
{
|
||||
session.Issues.Add(new ImportAuditIssue
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
AuditRunId = session.Run.Id,
|
||||
Entity = entity,
|
||||
FieldName = fieldName,
|
||||
SourceValue = sourceValue.HasValue
|
||||
? sourceValue.Value.ToString(CultureInfo.InvariantCulture)
|
||||
: null,
|
||||
CanonicalValue = canonicalValue.ToString(CultureInfo.InvariantCulture),
|
||||
Resolution = DefaultResolution,
|
||||
Severity = string.IsNullOrWhiteSpace(severity) ? "WARNING" : severity.Trim().ToUpperInvariant(),
|
||||
CreatedAt = DateTime.UtcNow
|
||||
});
|
||||
}
|
||||
|
||||
return canonicalValue;
|
||||
}
|
||||
|
||||
public async Task SaveRunAsync(
|
||||
SpreadsheetImportAuditSession session,
|
||||
string status = "SUCCESS",
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(session);
|
||||
|
||||
session.Run.Status = string.IsNullOrWhiteSpace(status) ? "SUCCESS" : status.Trim().ToUpperInvariant();
|
||||
session.Run.UpdatedAt = DateTime.UtcNow;
|
||||
|
||||
_db.ImportAuditRuns.Add(session.Run);
|
||||
|
||||
if (session.Issues.Count > 0)
|
||||
{
|
||||
_db.ImportAuditIssues.AddRange(session.Issues);
|
||||
}
|
||||
|
||||
await _db.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<int> GetCanonicalTotalLinhasForReadAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var fromLatestRun = await _db.ImportAuditRuns
|
||||
.AsNoTracking()
|
||||
.OrderByDescending(x => x.ImportedAt)
|
||||
.ThenByDescending(x => x.Id)
|
||||
.Select(x => (int?)x.CanonicalTotalLinhas)
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
|
||||
if (fromLatestRun.HasValue && fromLatestRun.Value > 0)
|
||||
{
|
||||
return fromLatestRun.Value;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Fallback para evitar indisponibilidade caso a migration ainda não tenha sido aplicada.
|
||||
}
|
||||
|
||||
return await CalculateCanonicalTotalLinhasFromGeralAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<int> CalculateCanonicalTotalLinhasFromGeralAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var maxItem = await _db.MobileLines
|
||||
.AsNoTracking()
|
||||
.MaxAsync(x => (int?)x.Item, cancellationToken);
|
||||
|
||||
if (maxItem.HasValue && maxItem.Value > 0)
|
||||
{
|
||||
return maxItem.Value;
|
||||
}
|
||||
|
||||
return await _db.MobileLines.AsNoTracking().CountAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class SpreadsheetImportAuditSession
|
||||
{
|
||||
internal SpreadsheetImportAuditSession(ImportAuditRun run)
|
||||
{
|
||||
Run = run;
|
||||
}
|
||||
|
||||
public ImportAuditRun Run { get; }
|
||||
|
||||
public List<ImportAuditIssue> Issues { get; } = new();
|
||||
|
||||
public int CanonicalTotalLinhas => Run.CanonicalTotalLinhas;
|
||||
}
|
||||
|
|
@ -1,4 +1,6 @@
|
|||
using System.Security.Claims;
|
||||
using line_gestao_api.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace line_gestao_api.Services;
|
||||
|
||||
|
|
@ -11,20 +13,61 @@ public class TenantMiddleware
|
|||
_next = next;
|
||||
}
|
||||
|
||||
public async Task InvokeAsync(HttpContext context, ITenantProvider tenantProvider)
|
||||
public async Task InvokeAsync(
|
||||
HttpContext context,
|
||||
ITenantProvider tenantProvider,
|
||||
AppDbContext db)
|
||||
{
|
||||
Guid? tenantId = null;
|
||||
var claim = context.User.FindFirst("tenantId")?.Value
|
||||
?? context.User.FindFirst("tenant")?.Value;
|
||||
var headerValue = context.Request.Headers["X-Tenant-Id"].FirstOrDefault();
|
||||
|
||||
if (Guid.TryParse(claim, out var parsed))
|
||||
// Usuário autenticado: tenant vem do token.
|
||||
// Se token legado vier sem tenantId, tenta resolver pelo UserId no banco.
|
||||
if (context.User.Identity?.IsAuthenticated == true)
|
||||
{
|
||||
tenantId = parsed;
|
||||
var claim = context.User.FindFirst("tenantId")?.Value
|
||||
?? context.User.FindFirst("tenant")?.Value;
|
||||
|
||||
if (Guid.TryParse(claim, out var parsedFromClaim) && parsedFromClaim != Guid.Empty)
|
||||
{
|
||||
tenantId = parsedFromClaim;
|
||||
}
|
||||
else
|
||||
{
|
||||
var userIdRaw = context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value
|
||||
?? context.User.FindFirst("sub")?.Value;
|
||||
|
||||
if (Guid.TryParse(userIdRaw, out var userId))
|
||||
{
|
||||
var tenantFromUser = await db.Users
|
||||
.AsNoTracking()
|
||||
.IgnoreQueryFilters()
|
||||
.Where(u => u.Id == userId)
|
||||
.Select(u => (Guid?)u.TenantId)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
if (tenantFromUser.HasValue && tenantFromUser.Value != Guid.Empty)
|
||||
{
|
||||
tenantId = tenantFromUser.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Evita comportamento silencioso de "dados vazios" quando token não traz tenant válido.
|
||||
if (!tenantId.HasValue)
|
||||
{
|
||||
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
|
||||
await context.Response.WriteAsync("Tenant inválido.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
else if (Guid.TryParse(headerValue, out var headerTenant))
|
||||
else
|
||||
{
|
||||
tenantId = headerTenant;
|
||||
// Usuário anônimo: permite header para fluxos como login multi-tenant.
|
||||
var headerValue = context.Request.Headers["X-Tenant-Id"].FirstOrDefault();
|
||||
if (Guid.TryParse(headerValue, out var headerTenant) && headerTenant != Guid.Empty)
|
||||
{
|
||||
tenantId = headerTenant;
|
||||
}
|
||||
}
|
||||
|
||||
tenantProvider.SetTenantId(tenantId);
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@ namespace line_gestao_api.Tests
|
|||
.UseInMemoryDatabase(Guid.NewGuid().ToString())
|
||||
.Options;
|
||||
|
||||
return new AppDbContext(options, provider);
|
||||
return new AppDbContext(options, provider, new NullAuditLogBuilder());
|
||||
}
|
||||
|
||||
private sealed class TestTenantProvider : ITenantProvider
|
||||
|
|
@ -112,5 +112,13 @@ namespace line_gestao_api.Tests
|
|||
TenantId = tenantId;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class NullAuditLogBuilder : IAuditLogBuilder
|
||||
{
|
||||
public List<AuditLog> BuildAuditLogs(Microsoft.EntityFrameworkCore.ChangeTracking.ChangeTracker changeTracker)
|
||||
{
|
||||
return new List<AuditLog>();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue