Skip to content

Repository files navigation

eContabil

Plataforma para escritórios de contabilidade que elimina o download manual de XMLs na SEFAZ.

Cada empresa-cliente é cadastrada uma vez, com seu certificado digital A1. A partir daí a plataforma consulta a Distribuição DF-e, manifesta ciência quando necessário para liberar o XML completo, arquiva os XMLs em object storage e entrega ao contador uma consulta única, filtrável e auditável de tudo que foi emitido contra ou pelos seus clientes.

A cadência tem duas escalas que vale não confundir: o ciclo do agendador roda de 5 em 5 minutos, e é ele que enfileira quem está vencido; cada empresa é consultada uma vez por hora, que é o mínimo que a SEFAZ tolera depois de uma resposta sem novidade. A grade fina existe porque uma empresa que vencia poucos segundos depois do tique perdia a janela inteira e o intervalo real virava uma hora e meia.

Os eventos também são interpretados: cancelamento e carta de correção alteram o documento. Sem isso, a nota que o emitente desfez continuaria marcada como autorizada e somando nos totais — o XML da nota original não muda quando ela é cancelada, só o evento diz.

Stack

Camada Tecnologia
Backend .NET 10 · ASP.NET Core · Clean Architecture + DDD + CQRS (MediatR 12)
Banco PostgreSQL 17 · EF Core 10 (Npgsql)
Storage MinIO (S3-compatible)
Agendamento Hangfire
Resiliência Polly 8 (nova tentativa no object storage)
Fiscal ZeusFiscal (Hercules.NET.NFe.NFCe)
Frontend Angular · Signals · NgRx SignalStore · Angular Material
Infra Docker + Docker Compose

Estado atual

As 14 etapas estão concluídas.

✅ 1. Estrutura da solução        ✅  8. Autenticação JWT + certificados
✅ 2. Docker + Docker Compose     ✅  9. Hangfire
✅ 3. Configuração inicial        ✅ 10. ZeusFiscal
✅ 4. Domain                      ✅ 11. MinIO
✅ 5. Application                 ✅ 12. Frontend Angular
✅ 6. Infrastructure              ✅ 13. Testes
✅ 7. API                         ✅ 14. Documentação

O backend está completo: captura automática de NF-e na SEFAZ, extração dos dados fiscais do XML (natureza da operação, totais de imposto e itens com CFOP, NCM e CST), manifestação de ciência, captura de eventos, cofre de certificados cifrado, autenticação com perfis, agendamento e a borda endurecida (CORS, limite de requisições, cabeçalhos de segurança e exceção não tratada virando ProblemDetails).

O frontend também: sessão com renovação silenciosa, rotas protegidas por perfil e as sete telas — painel, empresas, certificados, documentos, detalhe do documento, perfil e login.

Eventos: cancelamento e carta de correção

O XML de uma NF-e não muda quando ela é cancelada — só o evento diz. Uma versão anterior arquivava o evento e descartava o conteúdo, então nota desfeita pelo emitente ficava marcada como autorizada para sempre e continuava somando nos totais. Erro de apuração, não de tela.

Hoje todo evento vira registro, e três deles alteram o documento:

Código Evento Efeito
110111 Cancelamento situação passa a cancelada
110112 Cancelamento por substituição idem
110110 Carta de correção grava o texto vigente; a de maior sequência prevalece

O resto — rastreamento de CT-e e MDF-e, passagem em posto fiscal, e o eco da própria ciência que a plataforma envia — aparece no histórico da nota e não a altera. É a esmagadora maioria do volume: no acervo de referência, 402 eventos e apenas um alterava documento.

Duas decisões que sustentam isso:

  • O evento chega antes da nota. Já aconteceu em produção: um cancelamento cuja NF-e nunca foi capturada, porque a empresa não era parte dela. Por isso o evento é entidade própria, e não filha do documento — como filha, não haveria onde guardá-lo, e a informação seria descartada. Quando a nota aparece, o evento pendente é aplicado.
  • O cStat do retorno é a guarda. Evento recusado pela SEFAZ é registrado e não altera nada. Aplicar cancelaria uma nota válida.

Nos números: documento cancelado continua listado — guarda fiscal de cinco anos —, mas sai de todo somatório. O resumo da grade e o relatório por empresa expõem cancelados e valorCancelado à parte, para a conta fechar na conferência.

Há um POST /api/v1/jobs/reprocessar-eventos para o acervo capturado antes de a interpretação existir. Não fala com a SEFAZ — lê só o object storage —, então roda com o CNPJ bloqueado por consumo indevido.

Um ponto cego conhecido

Na rejeição 656 (consumo indevido) o ponteiro salta para o NSU que a SEFAZ informou, e tudo no intervalo é perdido — documento e evento —, porque NSU consumido não é reoferecido. Já aconteceu: 188 NSU descartados numa única rejeição.

A reconciliação por chave (NfeConsultaProtocolo) é o que fecha isso, e está especificada mas não implementada. Ela é conferência sob demanda, nunca varredura periódica: é uma chamada por documento, contra a mesma cota que gera o 656 — um recorrente viraria a causa do problema que conserta.

A captura já foi exercitada contra a SEFAZ de produção: NF-e reais entraram pela Distribuição DF-e, com XML completo arquivado no object storage e o ponteiro de NSU avançando entre as rodadas.

O XML é arquivado descompactado. A Distribuição DF-e entrega cada documento no elemento docZip — o arquivo comprimido em gzip e codificado em base64 —, e a biblioteca fiscal decodifica o base64 mas devolve os bytes ainda comprimidos. Arquivá-los como vieram produz .xml que nenhum leitor abre.

Uma ressalva que vale para qualquer instalação: o ponteiro de NSU pertence ao CNPJ, não à aplicação. Se outro sistema fiscal já consome a fila daquele CNPJ, a carga inicial não traz histórico — os NSUs anteriores já foram entregues a ele. Não é defeito, é como a SEFAZ define a fila.

A manifestação de ciência também já foi exercitada em produção: o evento 210210 foi aceito pela SEFAZ e o protocolo ficou gravado no documento.

As pendências conhecidas — todas registradas como decisão, não esquecimento — estão em RELEASE.md.

Fluxo mínimo para testar de ponta a ponta

Todas as rotas exigem autenticação. O primeiro administrador é criado no boot a partir de ECONTABIL_ADMIN_EMAIL e ECONTABIL_ADMIN_SENHA.

# 0. autenticar (o refresh token vem em cookie httpOnly; o access token, no corpo)
curl -X POST http://localhost:8080/api/v1/auth/login -H "Content-Type: application/json" \
  -d '{"email":"admin@escritorio.com.br","senha":"<senha>"}'

# nas chamadas seguintes: -H "Authorization: Bearer <accessToken>"

# 1. cadastrar a empresa-cliente
curl -X POST http://localhost:8080/api/v1/empresas -H "Content-Type: application/json" \
  -d '{"razaoSocial":"EMPRESA LTDA","cnpj":"00000000000000","uf":"SP","ambiente":1}'

# 2. enviar o certificado A1 (o CNPJ do titular precisa bater com o da empresa)
curl -X POST http://localhost:8080/api/v1/certificados \
  -F "empresaId=<id>" -F "senha=<senha>" -F "arquivo=@caminho/do/certificado.pfx"

# 3. sincronizar agora, sem esperar a janela
#    (o passo 2 já enfileirou a carga inicial sozinho; isto é só para forçar na hora)
curl -X POST "http://localhost:8080/api/v1/jobs/sincronizar/<id>?modo=CargaInicial"

# ou em segundo plano, respondendo na hora com o id do trabalho
curl -X POST "http://localhost:8080/api/v1/jobs/enfileirar/<id>?modo=CargaInicial"

# 4. consultar o que veio
curl "http://localhost:8080/api/v1/documentos?empresaId=<id>"
curl "http://localhost:8080/api/v1/documentos/<documentoId>/detalhe"
curl -OJ "http://localhost:8080/api/v1/documentos/<documentoId>/xml"

# filtrar por dados fiscais: CFOP e NCM aceitam prefixo (5 traz as saídas, 3004 traz a família)
curl "http://localhost:8080/api/v1/documentos?cfop=5102&ncm=3004&comSubstituicaoTributaria=true"

# totais do mesmo recorte da listagem (aceita os mesmos filtros)
curl "http://localhost:8080/api/v1/documentos/resumo?empresaId=<id>"

# acervo consolidado por empresa, para conferência do escritório
curl "http://localhost:8080/api/v1/documentos/relatorio/por-empresa?de=2026-07-01&ate=2026-07-31"

# 5. ver o que cada execução fez
curl "http://localhost:8080/api/v1/jobs/execucoes?empresaId=<id>&quantidade=20"

# 6. registrar a ciência agora, sem esperar o ciclo
curl -X POST "http://localhost:8080/api/v1/jobs/manifestar/<id>"

# 7. criar um operador (só Admin pode)
curl -X POST http://localhost:8080/api/v1/usuarios -H "Content-Type: application/json" \
  -d '{"nome":"Operador","email":"operador@escritorio.com.br","senha":"<12+ caracteres>","role":2}'

Nem empresa nem usuário são excluídos: PATCH /api/v1/empresas/<id>/status e PATCH /api/v1/usuarios/<id>/status inativam e reativam. Inativar uma empresa a tira do ciclo de captura e preserva o acervo, que tem guarda fiscal de cinco anos; o ponteiro de NSU não é tocado, então reativar retoma de onde parou. CNPJ e ambiente da SEFAZ não são editáveis — o ponteiro pertence ao CNPJ.

DELETE /api/v1/certificados/<id> apaga o arquivo cifrado do cofre e mantém o registro inativo: é ele que explica qual identidade fiscal assinou as consultas já feitas.

Disparo manual para uma empresa que já tem captura em andamento responde 409 com o identificador do trabalho, em vez de ficar esperando.

GET /health/ready lista PostgreSQL, MinIO e SEFAZ separadamente. A SEFAZ conta como degradação, não como reprovação: sem ela a captura fica suspensa, mas a grade e os downloads seguem funcionando.

A inativação de usuário derruba as sessões abertas na hora. O último administrador ativo não pode ser inativado nem rebaixado — sem ele ninguém administra o sistema. Não há recuperação de senha por e-mail: o administrador redefine em POST /api/v1/usuarios/<id>/senha, o que também encerra as sessões do usuário.

Nota de fornecedor chega como resumo: a SEFAZ só libera o XML completo depois que a ciência da operação é registrada. Isso acontece sozinho — a sincronização enfileira a manifestação quando traz documento novo, e um ciclo recorrente varre os pendentes. O XML não volta na resposta do evento: ele reaparece num NSU posterior e a sincronização seguinte o arquiva.

A captura roda sozinha: o ciclo avalia quem está vencido de 5 em 5 minutos, e cada empresa é consultada uma vez por hora. O disparo manual serve para não esperar a janela. O dashboard do Hangfire fica em /hangfire, aberto no navegador depois do login — a sessão vai por cookie restrito a essa rota, e só perfil Admin entra.

Empresa cujo CNPJ já é consultado por outro sistema fiscal responde 656 na primeira sincronização e fica bloqueada por uma hora — é o comportamento esperado, e o ultimoNsu já sai realinhado pelo valor que a própria SEFAZ informou.

Migrations são geradas e aplicadas com a ferramenta local do repositório:

dotnet tool restore
dotnet ef database update -p src/eContabil.Infrastructure -s src/eContabil.Infrastructure

Frontend

O app fica em src/econtabil-web (Angular 21, zoneless, NgRx SignalStore, Angular Material, Vitest).

cd src/econtabil-web
npm install
npm start          # http://localhost:4200, contra a API em execução
npm run build
npm test           # Vitest

Ou pelo Compose, junto do resto — o nginx serve o app e faz proxy de /api/ para a API, o que mantém tudo na mesma origem:

docker compose --profile frontend up -d --build

O access token vive só em memória; o refresh token é um cookie httpOnly que o JavaScript não lê. Uma sessão expirada é renovada uma única vez, mesmo com várias requisições em voo.

Estrutura

eContabil/
├── src/
│   ├── eContabil.Shared/          Result, Notification, primitivos — zero dependências
│   ├── eContabil.Domain/          entidades, value objects, invariantes — zero dependências
│   ├── eContabil.Application/     commands, queries, handlers, behaviors
│   ├── eContabil.Infrastructure/  EF Core, MinIO, ZeusFiscal, Hangfire, JWT, criptografia
│   ├── eContabil.Api/             controllers, middlewares, Swagger, composition root
│   └── econtabil-web/             Angular 21 — core (auth, http, layout), features, shared
└── tests/
    ├── eContabil.Domain.Tests/
    ├── eContabil.Application.Tests/
    ├── eContabil.Infrastructure.Tests/
    └── eContabil.Api.IntegrationTests/

Dependências apontam para dentro: Api → Application → Domain → Shared. Infrastructure implementa as interfaces declaradas na Application; Api só a conhece para registrar a injeção de dependência.

Requisitos

  • .NET SDK 10.0.100+
  • Docker Desktop (a partir da etapa 2)
  • Node 22+ (a partir da etapa 12)

Como rodar

Sem Docker

dotnet build eContabil.slnx     # deve terminar com 0 avisos e 0 erros
dotnet test  eContabil.slnx

Com Docker

cp .env.example .env            # e troque TODOS os valores antes de qualquer uso real
docker compose up -d --build

Sobem quatro serviços: postgres, minio, minio-init (cria os buckets e encerra) e api.

Endereço O quê
http://localhost:8080/swagger Documentação interativa da API
http://localhost:8080/health Health check da API — deve responder Healthy
http://localhost:9001 Console do MinIO
localhost:5432 PostgreSQL, publicado apenas para inspeção local

O serviço do frontend existe no Compose sob o perfil frontend e passa a ser usado na etapa 12: docker compose --profile frontend up.

docker compose ps               # todos devem estar healthy
docker compose logs -f api
docker compose down             # para tudo, preservando os dados
docker compose down -v          # APAGA banco, XMLs e certificados

Convenções de código

Comentários

Comentários descrevem o código — nada além disso. Explicam o quê e, principalmente, o porquê técnico de uma decisão no ponto em que ela existe.

Um comentário nunca referencia especificação, ADR, critério de aceite (AC-N), número de tarefa, decisão datada ou caminho de documento. Esse tipo de rastreabilidade vive nos .md, não no código: o documento muda de nome, é renumerado ou deixa de existir, e o comentário vira uma pista falsa que ninguém consegue mais verificar. Quem lê o código precisa entender o código ali mesmo, sem abrir outro arquivo.

// ❌ referencia documento externo — apodrece e não ajuda quem está lendo o código
// valida o NSU conforme spec 010, AC-8

// ❌ descreve o óbvio
// incrementa o NSU
UltimoNSU = novoNsu;

// ✅ explica o porquê técnico, com informação que o código sozinho não dá
// NSU que retrocede faz o mesmo lote ser reprocessado para sempre e mascara nota faltando
if (novoNsu <= UltimoNSU) return Result.Falhou(...);

// ✅ explica uma escolha não óbvia
// EphemeralKeySet: sem ele o .NET grava material de chave privada no perfil do container
using var cert = X509CertificateLoader.LoadPkcs12(bytes, senha, X509KeyStorageFlags.EphemeralKeySet);

Vale para comentários de linha e para XML docs. Comentário que só repete o nome do método deve ser apagado, não reescrito.

Regras que o código respeita

  • Mapeamento manual. AutoMapper, Mapster e TinyMapper são proibidos.
  • Regra de negócio nunca vira exceção — retorna Result com notificações. Exceção é para o inesperado.
  • XML nunca entra no banco; certificado nunca sai do cofre em claro, nunca toca disco, nunca vai para log.
  • Toda data é UTC no banco; conversão para America/Sao_Paulo só na apresentação.
  • Nenhum segredo em appsettings versionado — só variável de ambiente.
  • dotnet build com 0 avisos e dotnet test verde são pré-requisito de todo commit.

Branches

Uma branch por etapa, nomeada pela spec que ela implementa:

feature/<NNN>-<slug-da-spec>      feature/013-docker-compose
fix/<slug>                        fix/nsu-retrocede-em-lote-parcial

main só recebe trabalho concluído: build com 0 avisos, testes verdes e a spec correspondente atendida.

Commits

<tipo>(<escopo>): <resumo no imperativo>

O resumo se sustenta sozinho. Não há sufixo apontando para spec: elas são locais e não versionadas, e quem lê o histórico num clone não teria como abrir a referência. O corpo do commit é o lugar de explicar o porquê, com a informação inteira e não com um ponteiro.

Tipos: feat, fix, refactor, test, docs, chore, perf, build. Escopos: domain, application, infra, api, auth, sefaz, minio, hangfire, web, docker, db.

A referência à spec fica na mensagem do commit, que é imutável e datada — nunca no código.

Documentação

Documento Para quê
RUNBOOK.md Operação: backup, restore, rotação da chave mestra, incidentes
CONTRIBUTING.md Convenção de commits e regras do código
RELEASE.md Checklist antes de marcar uma versão
/swagger Contrato da API, gerado em tempo de execução

A documentação de arquitetura (docs/) e as especificações (specs/) são locais e não versionadas, por decisão do autor. Não estão neste repositório; o backup delas é responsabilidade do desenvolvedor.

Antes de subir em produção, leia o RUNBOOK.md. ECONTABIL_CRYPTO_KEY cifra os certificados digitais dos clientes: perdê-la inutiliza todos eles, sem recuperação possível.

Segurança

Este sistema armazena certificados digitais de terceiros. Antes de qualquer implantação, dois pontos inegociáveis:

  • A chave mestra de criptografia (ECONTABIL_CRYPTO_KEY) é a única coisa que torna os certificados recuperáveis. Perdê-la inutiliza todos eles. Backup obrigatório, em cofre separado do backup do banco.
  • Nenhum .pfx, .p12, .key ou .env pode ser versionado. O .gitignore já bloqueia todos eles.

About

Captura automática de NF-e da SEFAZ para escritórios de contabilidade: Distribuição DF-e com certificado A1, manifestação de ciência, eventos e consulta fiscal filtrável.

Topics

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages