| {{ execucao.iniciadoEm | dataHoraLocal }} |
+ {{ execucao.razaoSocial }} |
{{ execucao.duracaoSegundos | number: '1.0-1' }} s |
{{ execucao.documentosNovos }} |
{{ rotuloDoResultado(execucao) }} |
diff --git a/src/econtabil-web/src/app/features/dashboard/dashboard.model.ts b/src/econtabil-web/src/app/features/dashboard/dashboard.model.ts
index c09a221..4c4afc7 100644
--- a/src/econtabil-web/src/app/features/dashboard/dashboard.model.ts
+++ b/src/econtabil-web/src/app/features/dashboard/dashboard.model.ts
@@ -10,6 +10,7 @@ export enum ResultadoExecucao {
export interface ExecucaoLista {
readonly id: string;
readonly empresaId: string;
+ readonly razaoSocial: string;
readonly modo: number;
readonly iniciadoEm: string;
readonly finalizadoEm: string;
diff --git a/tests/eContabil.Application.Tests/Sincronizacao/ObterExecucoesQueryHandlerTests.cs b/tests/eContabil.Application.Tests/Sincronizacao/ObterExecucoesQueryHandlerTests.cs
index cc21c9b..d14969d 100644
--- a/tests/eContabil.Application.Tests/Sincronizacao/ObterExecucoesQueryHandlerTests.cs
+++ b/tests/eContabil.Application.Tests/Sincronizacao/ObterExecucoesQueryHandlerTests.cs
@@ -11,8 +11,7 @@ public class ObterExecucoesQueryHandlerTests
{
private static readonly DateTime AgoraUtc = new(2026, 7, 28, 12, 0, 0, DateTimeKind.Utc);
- private readonly IExecucaoSincronizacaoRepository _execucoes =
- Substitute.For();
+ private readonly IExecucaoConsultas _execucoes = Substitute.For();
[Theory]
[InlineData(0, 1)]
@@ -31,7 +30,7 @@ public async Task Handle_LimitaAQuantidadePedida(int pedida, int esperada)
}
[Fact]
- public async Task Handle_ProjetaADuracaoEmSegundos()
+ public async Task Handle_DerivaADuracaoDosDoisInstantes()
{
var empresaId = Guid.CreateVersion7();
@@ -47,13 +46,30 @@ public async Task Handle_ProjetaADuracaoEmSegundos()
Assert.Equal(ResultadoExecucao.Sucesso, execucao.Resultado);
}
+ [Fact]
+ public async Task Handle_EntregaARazaoSocialDeCadaExecucao()
+ {
+ // O histórico intercala empresas: duas linhas do mesmo minuto, uma concluída e outra bloqueada,
+ // não diziam a qual cadastro cada uma pertencia.
+ var empresaId = Guid.CreateVersion7();
+
+ _execucoes.ObterUltimasAsync(null, Arg.Any(), Arg.Any())
+ .Returns([ExecucaoDe(empresaId, TimeSpan.FromSeconds(2), "IMEVE INDUSTRIA LTDA")]);
+
+ var resultado = await Executar(new ObterExecucoesQuery(null, 10));
+
+ var execucao = Assert.Single(resultado.Valor);
+ Assert.Equal("IMEVE INDUSTRIA LTDA", execucao.RazaoSocial);
+ }
+
private Task>> Executar(ObterExecucoesQuery consulta) =>
new ObterExecucoesQueryHandler(_execucoes)
.Handle(consulta, TestContext.Current.CancellationToken);
- private static ExecucaoSincronizacao ExecucaoDe(Guid empresaId, TimeSpan duracao) =>
- ExecucaoSincronizacao.Registrar(
- empresaId, ModoSincronizacao.Incremental, AgoraUtc, AgoraUtc.Add(duracao),
- nsuInicial: 100, nsuFinal: 150, documentosNovos: 5, xmlsArquivados: 5,
- documentosPromovidos: 1, itensDescartados: 0, ResultadoExecucao.Sucesso, mensagem: null);
+ private static ExecucaoListaDto ExecucaoDe(
+ Guid empresaId, TimeSpan duracao, string razaoSocial = "EMPRESA TESTE LTDA") => new(
+ Guid.CreateVersion7(), empresaId, razaoSocial, ModoSincronizacao.Incremental,
+ AgoraUtc, AgoraUtc.Add(duracao), NsuInicial: 100, NsuFinal: 150, DocumentosNovos: 5,
+ XmlsArquivados: 5, DocumentosPromovidos: 1, ItensDescartados: 0,
+ ResultadoExecucao.Sucesso, Mensagem: "Concluída.");
}
diff --git a/tests/eContabil.Infrastructure.Tests/Persistencia/ExecucaoConsultasTests.cs b/tests/eContabil.Infrastructure.Tests/Persistencia/ExecucaoConsultasTests.cs
new file mode 100644
index 0000000..cd8cda9
--- /dev/null
+++ b/tests/eContabil.Infrastructure.Tests/Persistencia/ExecucaoConsultasTests.cs
@@ -0,0 +1,115 @@
+using eContabil.Domain.Empresas;
+using eContabil.Domain.Sincronizacoes;
+using eContabil.Domain.ValueObjects;
+using eContabil.Infrastructure.Persistencia.Consultas;
+using Xunit;
+
+namespace eContabil.Infrastructure.Tests.Persistencia;
+
+///
+/// O histórico de execuções, com o nome da empresa em cada linha.
+///
+///
+/// A junção só se prova contra banco real: com substituto no lugar da consulta, nem o filtro global de
+/// Empresa nem a tradução da subconsulta existem.
+///
+[Collection(nameof(PostgresTestes))]
+public class ExecucaoConsultasTests(PostgresFixture postgres)
+{
+ private static readonly DateTime AgoraUtc = new(2026, 7, 28, 12, 0, 0, DateTimeKind.Utc);
+
+ [Fact]
+ public async Task ObterUltimasAsync_TrazARazaoSocialDeCadaExecucao()
+ {
+ if (postgres.MotivoIndisponivel is { } motivo)
+ {
+ Assert.Skip(motivo);
+ return;
+ }
+
+ await postgres.LimparAsync();
+ await SemearAsync("PRIMEIRA EMPRESA LTDA", "11222333000181", ativa: true);
+ await SemearAsync("SEGUNDA EMPRESA LTDA", "11444777000161", ativa: true);
+
+ await using var contexto = postgres.CriarContexto();
+ var consultas = new ExecucaoConsultas(contexto);
+
+ var execucoes = await consultas.ObterUltimasAsync(
+ empresaId: null, quantidade: 10, TestContext.Current.CancellationToken);
+
+ Assert.Equal(2, execucoes.Count);
+ Assert.Contains(execucoes, execucao => execucao.RazaoSocial == "PRIMEIRA EMPRESA LTDA");
+ Assert.Contains(execucoes, execucao => execucao.RazaoSocial == "SEGUNDA EMPRESA LTDA");
+ }
+
+ [Fact]
+ public async Task ObterUltimasAsync_DeEmpresaInativada_ContinuaTrazendoONome()
+ {
+ if (postgres.MotivoIndisponivel is { } motivo)
+ {
+ Assert.Skip(motivo);
+ return;
+ }
+
+ await postgres.LimparAsync();
+ await SemearAsync("EMPRESA DESLIGADA LTDA", "11222333000181", ativa: false);
+
+ await using var contexto = postgres.CriarContexto();
+ var consultas = new ExecucaoConsultas(contexto);
+
+ var execucoes = await consultas.ObterUltimasAsync(
+ empresaId: null, quantidade: 10, TestContext.Current.CancellationToken);
+
+ // O filtro global de `Empresa` esconderia a junção, e a execução de uma empresa desligada é
+ // justamente a que explica por que a captura dela parou. Sem `IgnoreQueryFilters` a linha viria
+ // sem nome ou a consulta estouraria.
+ var execucao = Assert.Single(execucoes);
+ Assert.Equal("EMPRESA DESLIGADA LTDA", execucao.RazaoSocial);
+ }
+
+ [Fact]
+ public async Task ObterUltimasAsync_DerivaADuracaoDosDoisInstantes()
+ {
+ if (postgres.MotivoIndisponivel is { } motivo)
+ {
+ Assert.Skip(motivo);
+ return;
+ }
+
+ await postgres.LimparAsync();
+ await SemearAsync("EMPRESA TESTE LTDA", "11222333000181", ativa: true);
+
+ await using var contexto = postgres.CriarContexto();
+ var consultas = new ExecucaoConsultas(contexto);
+
+ var execucoes = await consultas.ObterUltimasAsync(
+ empresaId: null, quantidade: 10, TestContext.Current.CancellationToken);
+
+ // A subtração dos dois `timestamptz` não é traduzida para segundos pelo provedor; o cálculo
+ // acontece depois de materializar, e o valor precisa sobreviver ao percurso.
+ Assert.Equal(90, Assert.Single(execucoes).DuracaoSegundos);
+ }
+
+ private async Task SemearAsync(string razaoSocial, string cnpj, bool ativa)
+ {
+ await using var contexto = postgres.CriarContexto();
+
+ var empresa = Empresa.Criar(
+ razaoSocial, null, Cnpj.Criar(cnpj).Valor, null,
+ Uf.Criar("SP").Valor, AmbienteSefaz.Producao, AgoraUtc).Valor;
+
+ if (!ativa)
+ {
+ empresa.Inativar();
+ }
+
+ contexto.Empresas.Add(empresa);
+
+ contexto.ExecucoesSincronizacao.Add(ExecucaoSincronizacao.Registrar(
+ empresa.Id, ModoSincronizacao.Incremental, AgoraUtc, AgoraUtc.AddSeconds(90),
+ nsuInicial: 100, nsuFinal: 150, documentosNovos: 5, xmlsArquivados: 5,
+ documentosPromovidos: 1, itensDescartados: 0, ResultadoExecucao.Sucesso, mensagem: null));
+
+ await contexto.SaveChangesAsync(TestContext.Current.CancellationToken);
+ }
+}
diff --git a/tests/eContabil.Infrastructure.Tests/Persistencia/PostgresFixture.cs b/tests/eContabil.Infrastructure.Tests/Persistencia/PostgresFixture.cs
index 3fa5160..d64903b 100644
--- a/tests/eContabil.Infrastructure.Tests/Persistencia/PostgresFixture.cs
+++ b/tests/eContabil.Infrastructure.Tests/Persistencia/PostgresFixture.cs
@@ -60,9 +60,12 @@ public async Task LimparAsync()
{
await using var contexto = CriarContexto();
+ // `execucoes_sincronizacao` não tem chave estrangeira para `empresas` — o histórico sobrevive à
+ // exclusão do cadastro de propósito. Como o CASCADE não a alcança, ela precisa ser nomeada aqui,
+ // senão as execuções de um teste vazam para o próximo como linhas órfãs.
await contexto.Database.ExecuteSqlRawAsync(
- "TRUNCATE documentos_fiscais, certificados_digitais, empresas, refresh_tokens, usuarios " +
- "RESTART IDENTITY CASCADE;");
+ "TRUNCATE documentos_fiscais, certificados_digitais, empresas, execucoes_sincronizacao, " +
+ "refresh_tokens, usuarios RESTART IDENTITY CASCADE;");
}
public async ValueTask DisposeAsync() => await _container.DisposeAsync();