diff --git a/src/eContabil.Application/Sincronizacao/Consultas/ExecucaoListaDto.cs b/src/eContabil.Application/Sincronizacao/Consultas/ExecucaoListaDto.cs index 5bab316..de5517f 100644 --- a/src/eContabil.Application/Sincronizacao/Consultas/ExecucaoListaDto.cs +++ b/src/eContabil.Application/Sincronizacao/Consultas/ExecucaoListaDto.cs @@ -3,14 +3,21 @@ namespace eContabil.Application.Sincronizacao.Consultas; -/// Uma execução de sincronização, como aparece no histórico. +/// +/// Uma execução de sincronização, como aparece no histórico. +/// +/// +/// A razão social vem junto porque o histórico é lido de cima para baixo, com várias empresas +/// intercaladas: duas linhas do mesmo minuto, uma concluída e outra bloqueada, não diziam a qual +/// cadastro cada uma pertencia. +/// public sealed record ExecucaoListaDto( Guid Id, Guid EmpresaId, + string RazaoSocial, ModoSincronizacao Modo, DateTime IniciadoEm, DateTime FinalizadoEm, - double DuracaoSegundos, long NsuInicial, long NsuFinal, int DocumentosNovos, @@ -18,4 +25,15 @@ public sealed record ExecucaoListaDto( int DocumentosPromovidos, int ItensDescartados, ResultadoExecucao Resultado, - string Mensagem); + string Mensagem) +{ + /// + /// Duração da execução, derivada dos dois instantes. + /// + /// + /// Calculada aqui, e não na projeção: a subtração de dois timestamptz devolve um intervalo + /// que o provedor não converte para segundos, e guardar o valor seria repetir no DTO um dado que os + /// dois campos já determinam. + /// + public double DuracaoSegundos => (FinalizadoEm - IniciadoEm).TotalSeconds; +} diff --git a/src/eContabil.Application/Sincronizacao/Consultas/IExecucaoConsultas.cs b/src/eContabil.Application/Sincronizacao/Consultas/IExecucaoConsultas.cs new file mode 100644 index 0000000..85a84f6 --- /dev/null +++ b/src/eContabil.Application/Sincronizacao/Consultas/IExecucaoConsultas.cs @@ -0,0 +1,15 @@ +namespace eContabil.Application.Sincronizacao.Consultas; + +/// +/// Leitura do histórico de execuções. +/// +/// +/// Fica fora do repositório porque a tela precisa da razão social da empresa junto de cada linha, e +/// isso é uma junção — o repositório entrega entidades de sincronização, que não conhecem o cadastro. +/// +public interface IExecucaoConsultas +{ + /// Últimas execuções, da mais recente para a mais antiga. + Task> ObterUltimasAsync( + Guid? empresaId, int quantidade, CancellationToken ct); +} diff --git a/src/eContabil.Application/Sincronizacao/Consultas/ObterExecucoesQueryHandler.cs b/src/eContabil.Application/Sincronizacao/Consultas/ObterExecucoesQueryHandler.cs index 1c93f8c..0a693d2 100644 --- a/src/eContabil.Application/Sincronizacao/Consultas/ObterExecucoesQueryHandler.cs +++ b/src/eContabil.Application/Sincronizacao/Consultas/ObterExecucoesQueryHandler.cs @@ -1,10 +1,9 @@ -using eContabil.Domain.Sincronizacoes; using eContabil.Shared; using MediatR; namespace eContabil.Application.Sincronizacao.Consultas; -public sealed class ObterExecucoesQueryHandler(IExecucaoSincronizacaoRepository execucoes) +public sealed class ObterExecucoesQueryHandler(IExecucaoConsultas execucoes) : IRequestHandler>> { private const int QuantidadeMinima = 1; @@ -19,24 +18,9 @@ public async Task>> Handle( // string arraste milhares de linhas para montar uma tela que mostra as últimas dezenas. var quantidade = Math.Clamp(request.Quantidade, QuantidadeMinima, QuantidadeMaxima); - var registros = await execucoes.ObterUltimasAsync(request.EmpresaId, quantidade, cancellationToken); + var registros = await execucoes.ObterUltimasAsync( + request.EmpresaId, quantidade, cancellationToken); - return Result>.Ok([.. registros.Select(Mapear)]); + return Result>.Ok(registros); } - - private static ExecucaoListaDto Mapear(ExecucaoSincronizacao execucao) => new( - execucao.Id, - execucao.EmpresaId, - execucao.Modo, - execucao.IniciadoEm, - execucao.FinalizadoEm, - execucao.Duracao.TotalSeconds, - execucao.NsuInicial, - execucao.NsuFinal, - execucao.DocumentosNovos, - execucao.XmlsArquivados, - execucao.DocumentosPromovidos, - execucao.ItensDescartados, - execucao.Resultado, - execucao.Mensagem); } diff --git a/src/eContabil.Domain/Sincronizacoes/IExecucaoSincronizacaoRepository.cs b/src/eContabil.Domain/Sincronizacoes/IExecucaoSincronizacaoRepository.cs index 904016c..e7baae7 100644 --- a/src/eContabil.Domain/Sincronizacoes/IExecucaoSincronizacaoRepository.cs +++ b/src/eContabil.Domain/Sincronizacoes/IExecucaoSincronizacaoRepository.cs @@ -2,7 +2,4 @@ namespace eContabil.Domain.Sincronizacoes; public interface IExecucaoSincronizacaoRepository : IRepository { - /// Últimas execuções, para o histórico do dashboard. - Task> ObterUltimasAsync( - Guid? empresaId, int quantidade, CancellationToken ct); } diff --git a/src/eContabil.Infrastructure/InfraestruturaServiceCollectionExtensions.cs b/src/eContabil.Infrastructure/InfraestruturaServiceCollectionExtensions.cs index 89e0aaf..03cbfc0 100644 --- a/src/eContabil.Infrastructure/InfraestruturaServiceCollectionExtensions.cs +++ b/src/eContabil.Infrastructure/InfraestruturaServiceCollectionExtensions.cs @@ -7,6 +7,7 @@ using eContabil.Application.Fiscal; using eContabil.Application.Sefaz; using eContabil.Application.Sincronizacao; +using eContabil.Application.Sincronizacao.Consultas; using eContabil.Application.Usuarios.Consultas; using eContabil.Domain; using eContabil.Domain.Certificados; @@ -77,6 +78,7 @@ public static IServiceCollection AddInfraestrutura(this IServiceCollection servi services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); // Relógio do sistema. Os testes substituem por FakeTimeProvider para exercitar bloqueio, // expiração de certificado e janela de consulta sem depender do relógio da máquina. diff --git a/src/eContabil.Infrastructure/Persistencia/Consultas/ExecucaoConsultas.cs b/src/eContabil.Infrastructure/Persistencia/Consultas/ExecucaoConsultas.cs new file mode 100644 index 0000000..0fd4b88 --- /dev/null +++ b/src/eContabil.Infrastructure/Persistencia/Consultas/ExecucaoConsultas.cs @@ -0,0 +1,47 @@ +using eContabil.Application.Sincronizacao.Consultas; +using Microsoft.EntityFrameworkCore; + +namespace eContabil.Infrastructure.Persistencia.Consultas; + +/// +/// Histórico de execuções projetado direto no banco, já com o nome da empresa. +/// +/// +/// A junção usa IgnoreQueryFilters por causa do filtro global de Empresa: a execução de +/// uma empresa inativada continua no histórico — é justamente ela que explica por que a captura parou — +/// e sem isto a linha viria sem nome ou faria a consulta estourar. +/// +public sealed class ExecucaoConsultas(EContabilDbContext contexto) : IExecucaoConsultas +{ + public async Task> ObterUltimasAsync( + Guid? empresaId, int quantidade, CancellationToken ct) + { + var consulta = contexto.ExecucoesSincronizacao.AsNoTracking().IgnoreQueryFilters(); + + if (empresaId is { } id) + { + consulta = consulta.Where(execucao => execucao.EmpresaId == id); + } + + return await consulta + .OrderByDescending(execucao => execucao.FinalizadoEm) + .Take(quantidade) + .Select(execucao => new ExecucaoListaDto( + execucao.Id, + execucao.EmpresaId, + contexto.Empresas.Where(empresa => empresa.Id == execucao.EmpresaId) + .Select(empresa => empresa.RazaoSocial).First(), + execucao.Modo, + execucao.IniciadoEm, + execucao.FinalizadoEm, + execucao.NsuInicial, + execucao.NsuFinal, + execucao.DocumentosNovos, + execucao.XmlsArquivados, + execucao.DocumentosPromovidos, + execucao.ItensDescartados, + execucao.Resultado, + execucao.Mensagem)) + .ToListAsync(ct); + } +} diff --git a/src/eContabil.Infrastructure/Persistencia/EContabilDbContext.cs b/src/eContabil.Infrastructure/Persistencia/EContabilDbContext.cs index ee0a557..1c4ac4c 100644 --- a/src/eContabil.Infrastructure/Persistencia/EContabilDbContext.cs +++ b/src/eContabil.Infrastructure/Persistencia/EContabilDbContext.cs @@ -1,6 +1,7 @@ using eContabil.Domain.Certificados; using eContabil.Domain.Documentos; using eContabil.Domain.Empresas; +using eContabil.Domain.Sincronizacoes; using eContabil.Domain.Usuarios; using Microsoft.EntityFrameworkCore; @@ -26,6 +27,8 @@ public sealed class EContabilDbContext(DbContextOptions opti public DbSet Usuarios => Set(); + public DbSet ExecucoesSincronizacao => Set(); + protected override void OnModelCreating(ModelBuilder modelBuilder) { ArgumentNullException.ThrowIfNull(modelBuilder); diff --git a/src/eContabil.Infrastructure/Persistencia/Repositorios/ExecucaoSincronizacaoRepository.cs b/src/eContabil.Infrastructure/Persistencia/Repositorios/ExecucaoSincronizacaoRepository.cs index 6af9a3e..73e7fa2 100644 --- a/src/eContabil.Infrastructure/Persistencia/Repositorios/ExecucaoSincronizacaoRepository.cs +++ b/src/eContabil.Infrastructure/Persistencia/Repositorios/ExecucaoSincronizacaoRepository.cs @@ -1,24 +1,8 @@ using eContabil.Domain.Sincronizacoes; -using Microsoft.EntityFrameworkCore; namespace eContabil.Infrastructure.Persistencia.Repositorios; public sealed class ExecucaoSincronizacaoRepository(EContabilDbContext contexto) : RepositorioBase(contexto), IExecucaoSincronizacaoRepository { - public async Task> ObterUltimasAsync( - Guid? empresaId, int quantidade, CancellationToken ct) - { - var consulta = Conjunto.AsNoTracking(); - - if (empresaId is { } id) - { - consulta = consulta.Where(execucao => execucao.EmpresaId == id); - } - - return await consulta - .OrderByDescending(execucao => execucao.FinalizadoEm) - .Take(Math.Clamp(quantidade, 1, 200)) - .ToListAsync(ct); - } } diff --git a/src/econtabil-web/src/app/features/dashboard/dashboard.component.spec.ts b/src/econtabil-web/src/app/features/dashboard/dashboard.component.spec.ts index 6793de6..455618d 100644 --- a/src/econtabil-web/src/app/features/dashboard/dashboard.component.spec.ts +++ b/src/econtabil-web/src/app/features/dashboard/dashboard.component.spec.ts @@ -1,15 +1,79 @@ import { provideHttpClient } from '@angular/common/http'; import { provideHttpClientTesting } from '@angular/common/http/testing'; import { provideRouter } from '@angular/router'; -import { render, screen } from '@testing-library/angular'; +import { render, screen, within } from '@testing-library/angular'; +import { of } from 'rxjs'; +import { DocumentosService } from '../documentos/documentos.service'; +import { EmpresasService } from '../empresas/empresas.service'; import { DashboardComponent } from './dashboard.component'; +import { DashboardService } from './dashboard.service'; +import { ExecucaoLista, ResultadoExecucao } from './dashboard.model'; + +const EXECUCAO: ExecucaoLista = { + id: '019faa02-c4ff-7832-9bee-f0eba912692a', + empresaId: '019fa9f6-b200-78f1-9d54-9c766fe493ca', + razaoSocial: 'RICARDO COELHO', + modo: 1, + iniciadoEm: '2026-07-29T15:30:09Z', + finalizadoEm: '2026-07-29T15:30:11Z', + duracaoSegundos: 1.6, + nsuInicial: 319438, + nsuFinal: 319478, + documentosNovos: 0, + xmlsArquivados: 40, + documentosPromovidos: 0, + itensDescartados: 0, + resultado: ResultadoExecucao.Sucesso, + mensagem: '40 eventos arquivados.', +}; + +async function montar(execucoes: readonly ExecucaoLista[]) { + return render(DashboardComponent, { + providers: [ + provideRouter([]), + provideHttpClient(), + provideHttpClientTesting(), + { provide: DashboardService, useValue: { execucoes: () => of(execucoes) } }, + { + provide: EmpresasService, + useValue: { + listar: () => + of({ itens: [], total: 0, pagina: 1, tamanho: 100, temProximaPagina: false }), + }, + }, + { provide: DocumentosService, useValue: { relatorioPorEmpresa: () => of([]) } }, + ], + }); +} describe('DashboardComponent', () => { it('anuncia a pagina com um cabecalho acessivel', async () => { - await render(DashboardComponent, { - providers: [provideRouter([]), provideHttpClient(), provideHttpClientTesting()], - }); + await montar([]); expect(screen.getByRole('heading', { name: /painel/i })).toBeInTheDocument(); }); + + it('identifica a empresa de cada execucao do historico', async () => { + // Duas empresas no mesmo minuto, uma concluída e outra bloqueada: sem a coluna não havia como + // saber a qual cadastro cada linha pertencia. + const bloqueada: ExecucaoLista = { + ...EXECUCAO, + id: '019faa03-2d37-707d-82b0-9c8ea8c8367a', + razaoSocial: 'IMEVE INDUSTRIA DE MEDICAMENTOS VETERINARIOS S A', + resultado: ResultadoExecucao.Bloqueada, + mensagem: 'Rejeitada por consumo indevido; nova tentativa em 1 hora.', + }; + + await montar([EXECUCAO, bloqueada]); + + expect(await screen.findByRole('columnheader', { name: /empresa/i })).toBeInTheDocument(); + + const linhaDoRicardo = screen.getByText('RICARDO COELHO').closest('tr'); + expect(within(linhaDoRicardo!).getByText('40 eventos arquivados.')).toBeInTheDocument(); + + const linhaDaImeve = screen + .getByText('IMEVE INDUSTRIA DE MEDICAMENTOS VETERINARIOS S A') + .closest('tr'); + expect(within(linhaDaImeve!).getByText(/consumo indevido/)).toBeInTheDocument(); + }); }); diff --git a/src/econtabil-web/src/app/features/dashboard/dashboard.component.ts b/src/econtabil-web/src/app/features/dashboard/dashboard.component.ts index f4cd955..9fe9465 100644 --- a/src/econtabil-web/src/app/features/dashboard/dashboard.component.ts +++ b/src/econtabil-web/src/app/features/dashboard/dashboard.component.ts @@ -152,6 +152,7 @@ const ROTULOS_DE_RESULTADO: Readonly> = { Início + Empresa Duração Documentos Resultado @@ -162,6 +163,7 @@ const ROTULOS_DE_RESULTADO: Readonly> = { @for (execucao of execucoes(); track execucao.id) { {{ 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();