Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,15 @@ Dockerfile text eol=lf
*.dockerfile text eol=lf
.dockerignore text eol=lf

# Fontes do frontend: LF sempre, inclusive na cópia de trabalho no Windows. O Prettier valida o fim de
# linha e usa LF por padrão; deixá-las no `text=auto` fazia o checkout no Windows gravar CRLF e
# `prettier --check` falhar na máquina do desenvolvedor enquanto passava no CI Linux. Rodar `--write`
# "resolvia" até o próximo checkout, e a falha voltava. Estes tipos só existem em `src/econtabil-web`.
*.ts text eol=lf
*.html text eol=lf
*.scss text eol=lf
*.css text eol=lf

# Nunca tratar como texto.
*.pfx binary
*.p12 binary
Expand Down
10 changes: 7 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,15 @@ dotnet build eContabil.slnx # 0 avisos
dotnet format eContabil.slnx --verify-no-changes
dotnet test eContabil.slnx

cd src/econtabil-web && npm run build && npm test
cd src/econtabil-web
npx prettier --check "src/**/*.{ts,html,scss}" # o CI recusa o que estiver fora do padrão
npm run build
npm test
```

Os três primeiros e os dois últimos precisam estar verdes. O CI roda o mesmo conjunto e recusa o
contrário.
Todos precisam estar verdes. O CI roda exatamente este conjunto e recusa o contrário — inclusive a
conferência do Prettier, que é a que mais escapa por rodar fora do `ng build`. Para corrigir de uma
vez: `npx prettier --write "src/**/*.{ts,html,scss}"`.

## Testes

Expand Down
17 changes: 14 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,16 @@ requisições, cabeçalhos de segurança e exceção não tratada virando Proble
**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.

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.

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.

As pendências conhecidas — todas registradas como decisão, não esquecimento — estão em
[`RELEASE.md`](RELEASE.md). A principal: o teste de ponta a ponta contra a SEFAZ de produção depende de
um CNPJ que ainda não seja consultado por outro sistema fiscal.
[`RELEASE.md`](RELEASE.md). A principal: a manifestação de ciência ainda não foi exercitada contra a
SEFAZ de produção.

### Fluxo mínimo para testar de ponta a ponta

Expand Down Expand Up @@ -272,9 +279,13 @@ fix/<slug> fix/nsu-retrocede-em-lote-parcial
### Commits

```
<tipo>(<escopo>): <resumo no imperativo> (spec NNN)
<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`.

Expand Down
22 changes: 15 additions & 7 deletions RELEASE.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
- [ ] `dotnet build eContabil.slnx` sem avisos
- [ ] `dotnet format eContabil.slnx --verify-no-changes` limpo
- [ ] `dotnet test eContabil.slnx` verde
- [ ] `npx prettier --check "src/**/*.{ts,html,scss}"` limpo em `src/econtabil-web`
- [ ] `npm run build` e `npm test` verdes em `src/econtabil-web`
- [ ] `docker compose --profile frontend up -d --build` sobe os cinco serviços saudáveis
- [ ] Nenhum `.pfx`, `.env` ou XML de cliente rastreado pelo git
Expand All @@ -27,6 +28,9 @@
- [ ] `/health/ready` com PostgreSQL, MinIO e SEFAZ
- [ ] `/hangfire` recusando anônimo e aceitando `Admin`
- [ ] Grade de documentos, detalhe e download de XML
- [ ] Totalizadores da grade e relatório por empresa batendo com a contagem no banco
- [ ] Manifestação de ciência: documento sai de `Pendente` e o protocolo aparece no detalhe
- [ ] Empresa bloqueada pela SEFAZ não recebe consulta nem evento até o prazo expirar

## Operação

Expand All @@ -37,15 +41,19 @@

## Pendências conhecidas do 1.0

Itens registrados como decisão, não esquecimento:
Itens adiados por decisão, não por esquecimento. A coluna diz o que o operador perde enquanto a
pendência existir — as especificações de origem são locais e não versionadas, então apontar para elas
daqui só criaria referência que ninguém consegue abrir.

| Pendência | Onde está registrado |
| Pendência | O que isso custa hoje |
|---|---|
| Imagem da API com 375 MB contra os 250 MB do NFR | spec 013 |
| Comando de rotação da chave mestra ainda não implementado | spec 008 e `RUNBOOK.md` |
| Manifestação não exercitada contra a SEFAZ de produção | spec 010 |
| Consulta avulsa por chave (`consChNFe`) | spec 010, decisão adiada |
| E2E de navegador e teste de carga | spec 016, decisões adiadas |
| Imagem da API com 375 MB contra os 250 MB do NFR | Deploy mais lento e mais banda por release; sem impacto em execução |
| Comando de rotação da chave mestra não implementado | Rotacionar `ECONTABIL_CRYPTO_KEY` exige o procedimento manual do `RUNBOOK.md`, com janela de parada |
| Manifestação não exercitada contra a SEFAZ de produção | O caminho de ciência só foi provado por teste; a primeira execução real pode revelar rejeição não prevista |
| Consulta avulsa por chave (`consChNFe`) | Nota conhecida por chave só entra no acervo se aparecer na fila de NSU |
| E2E de navegador e teste de carga | Regressão de fluxo completo e limite de carga dependem de verificação manual |
| Sem tela de usuários no frontend | Criar, editar e desativar operador só por API — `curl` ou Swagger |
| Sem troca da própria senha | O usuário depende de um `Admin` para redefinir; o próprio `Admin` precisa chamar o endpoint contra o próprio id |

## Marcar

Expand Down
5 changes: 3 additions & 2 deletions RUNBOOK.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,9 @@ Quando fazer: suspeita de vazamento, saída de alguém com acesso ao `.env`, ou
O modelo já prevê a rotação: cada certificado guarda a `versao_chave` com que foi cifrado. A rotação
decifra com a antiga e recifra com a nova, **um certificado por vez**, sem parar o sistema.

> **A implementação do comando de rotação ainda não existe** — está registrada como decisão adiada na
> spec 008. Enquanto não existir, a rotação exige o procedimento manual abaixo, com janela de parada.
> **A implementação do comando de rotação ainda não existe** — é decisão adiada, registrada nas
> pendências de [`RELEASE.md`](RELEASE.md). Enquanto não existir, a rotação exige o procedimento manual
> abaixo, com janela de parada.

### Procedimento manual, hoje

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,15 @@ public async Task<Result<ResultadoManifestacao>> Handle(
return "Empresa inativa.";
}

// O bloqueio por consumo indevido vale para o CNPJ, não para um webservice específico: a SEFAZ
// conta distribuição e eventos na mesma cota. Sem esta guarda o ciclo de manifestação continuaria
// batendo de meia em meia hora numa empresa que a sincronização já parou de consultar, renovando
// justamente o bloqueio que se está esperando expirar.
if (empresa.EstaBloqueada(agora))
{
return $"Empresa bloqueada pela SEFAZ até {empresa.BloqueadaAte:dd/MM/yyyy HH:mm} UTC.";
}

if (certificado is null)
{
return "Empresa sem certificado ativo.";
Expand Down Expand Up @@ -136,6 +145,17 @@ private async Task<Result<ResultadoManifestacao>> PercorrerLotesAsync(

await unitOfWork.SalvarAlteracoesAsync(ct);

// Lote inteiro sem desfecho: a SEFAZ processou a remessa mas não devolveu retorno para
// nenhuma das chaves. Elas continuam pendentes, então a próxima volta do laço buscaria
// exatamente o mesmo lote e o reenviaria — até o teto, multiplicando chamadas idênticas
// contra a cota do CNPJ. Quem retoma é o ciclo seguinte.
if (parcial.Registradas + parcial.JaRegistradas + parcial.Rejeitadas == 0)
{
return Result<ResultadoManifestacao>.Ok(new ResultadoManifestacao(
empresa.Id, registradas, jaRegistradas, rejeitadas, RestamPendentes: true,
Mensagem(registradas, jaRegistradas, rejeitadas)));
}

// Lote menor que o pedido significa que a fila de pendentes acabou.
if (pendentes.Count < _opcoes.ChavesPorLote)
{
Expand Down
9 changes: 9 additions & 0 deletions src/eContabil.Domain/Documentos/DocumentoFiscal.cs
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,15 @@ public Result PromoverParaXmlCompleto(string bucket, string objectName, string h
PossuiXml = true;
OrigemConteudo = OrigemConteudo.XmlCompleto;

// A ciência existe para destravar o XML completo. Com ele em mãos não sobra o que destravar, e
// manter o documento como pendente faria o job de manifestação reenviar o evento 210210 de uma
// nota já resolvida — chamada à SEFAZ sem propósito, do tipo que puxa rejeição e bloqueio do CNPJ.
// Ciência já registrada ou recusada preserva o próprio desfecho: são histórico do que aconteceu.
if (ManifestacaoStatus is ManifestacaoStatus.Pendente)
{
ManifestacaoStatus = ManifestacaoStatus.NaoAplicavel;
}

return Result.Ok();
}

Expand Down
Loading
Loading