Skip to content

fix: ler o envelope de eventos que a SEFIN devolve - #6

Merged
jonathanpmartins merged 2 commits into
mainfrom
fix/consulta-eventos-envelope-real
Aug 4, 2026
Merged

jonathanpmartins merged 2 commits into
mainfrom
fix/consulta-eventos-envelope-real

Conversation

@jonathanpmartins

Copy link
Copy Markdown
Contributor

O defeito

consultar()->eventos() exigia eventoXmlGZipB64 no topo da resposta. O SefinNacional_1.6.0 devolve uma lista eventos[], com o XML em eventos[].arquivoXml e duas camadas de base64 sobre o gzip.

Consequência: nenhuma consulta de evento existente podia ter sucesso. Ausência responde 404 e vira EVENT_NOT_FOUND — esse caminho sempre funcionou. Presença responde 200, não trazia o campo exigido e virava IndeterminateResultException.

Quanto mais bem-sucedida a operação no SEFIN, pior a leitura do SDK: a reconciliação de um cancelamento registrado concluía "não foi possível confirmar", e uma nota cancelada por análise fiscal seguia autorizada no banco do consumidor.

Por que a suíte não via

Os fixtures foram construídos a partir de EventosPostResponseSucesso, que é o que o próprio swagger aponta como resposta 200 do GET. Suíte verde contra um contrato que o servidor não cumpre. Os testes desta PR usam a codificação medida em produção, e a prova ponta-a-ponta passa pelo pipeline real.

O que muda

  • eventos() lê as duas formas — eventos[] primeiro, eventoXmlGZipB64 de topo depois. storage/prefeituras.json tem prefeituras de implementação própria que podem cumprir o contrato declarado.
  • EventsResponse::$eventos — lista de EventoConsultado (chaveAcesso, tipoEvento, numeroPedidoRegistroEvento, dataHoraRecebimento, xml, parseError). Item ilegível não derruba a lista.
  • GzipCompressor::decompressB64() descasca a camada extra, condicionada ao magic gzip. Seguro sem conferir antes: 0x1f está fora do alfabeto base64, então o decode estrito de um conteúdo já descomprimido falha sozinho.
  • IndeterminateResultException carrega statusCode, body (8 KiB) e raw. NfseFailed carrega o throwable.
  • executeRaw declara os campos exigidos pela forma que a operação lê ($requiredStrings / $requiredLists). A forma faz parte da exigência — aceitar qualquer valor não-vazio deixaria uma lista chegar a um construtor ?string e estourar TypeError.

Compatibilidade

Construtores públicos preservados: $previous continua na posição 3 em IndeterminateResultException, e throwable entrou por último em NfseFailed. A quebra é nas fábricas estáticas e em ExecutesNfseRequests (porta @internal).

Fora de escopo

  • O POST de evento (ParsesEventResponse). A varredura em produção cobriu só o GET, e a rejeição E0822 que chegou por aquela rota indica que o POST devolve eventoXmlGZipB64 mesmo. Só ganhou o raw na exceção.
  • A camada extra no ADN. O tratamento no GzipCompressor cobre DistribuicaoNSU.ArquivoXml se ela existir, mas nada foi medido.

Verificação

Check Resultado
pest --coverage --min=100 --parallel 100.0 % — 1462 testes, 3416 asserções
pest --mutate --min=100 --parallel 100.00 % — 1922 mutantes
pest --type-coverage --min=100 100.0 %
rector --dry-run OK
phpstan analyse No errors
psalm --taint-analysis No errors found
pint -p passed

Design em docs/superpowers/specs/2026-08-04-consulta-eventos-envelope-real-design.md.

🤖 Generated with Claude Code

jonathanpmartins and others added 2 commits August 4, 2026 07:34
A rota GET de eventos do SefinNacional 1.6.0 devolve `eventos[]` com o XML em
`eventos[].arquivoXml`, não o `eventoXmlGZipB64` de topo que o swagger declara
para ela. Nenhuma consulta de evento existente pode ter sucesso hoje: ausência
devolve 404 e funciona, presença devolve 200 e vira IndeterminateResultException.

Os fixtures dos testes foram construídos a partir do schema do swagger, então a
suíte ficou verde contra um contrato que o servidor não cumpre.

O spec cobre a leitura das duas formas, a camada extra de base64 sobre o gzip, e
a preservação de statusCode/body/raw na exceção — pedido do time consumidor, que
teve de reproduzir a chamada em produção porque a exceção não carregava nada.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
consultar()->eventos() exigia `eventoXmlGZipB64` no topo da resposta — o que
`EventosPostResponseSucesso` declara e o que o próprio swagger aponta como
resposta 200 do GET. O SefinNacional 1.6.0 devolve outra coisa: uma lista
`eventos[]`, com o XML em `eventos[].arquivoXml` e uma segunda camada de base64
sobre o gzip.

Só o caminho da ausência funcionava: evento inexistente responde 404 e vira
EVENT_NOT_FOUND; evento presente responde 200, não trazia o campo exigido e
virava IndeterminateResultException. Quanto mais bem-sucedida a operação no
SEFIN, pior a leitura do SDK — a reconciliação de um cancelamento registrado
concluía "não foi possível confirmar", e uma nota cancelada por análise fiscal
seguia autorizada no banco do consumidor.

A suíte não via nada porque os fixtures foram construídos a partir do schema do
swagger, não de tráfego real. Os testes novos usam a codificação medida em
produção, e a prova ponta-a-ponta passa pelo pipeline real.

As duas formas são aceitas: `eventos[]` primeiro, `eventoXmlGZipB64` de topo
depois — storage/prefeituras.json tem prefeituras de implementação própria que
podem cumprir o contrato declarado.

EventsResponse ganha `eventos`, lista de EventoConsultado com chaveAcesso,
tipoEvento, numeroPedidoRegistroEvento, dataHoraRecebimento, xml e parseError.
Um item ilegível não derruba a lista: os campos afetados vêm null e parseError
diz o que faltou, como DocumentoFiscal já fazia no lote de distribuição.

executeRaw passa a declarar os campos exigidos pela forma que a operação lê
($requiredStrings / $requiredLists). A forma faz parte da exigência: aceitar
qualquer valor não-vazio deixaria uma lista chegar a um construtor que declara
?string e estourar TypeError, fora do contrato de exceções do SDK.

Vai junto a preservação da evidência na exceção, que o mesmo incidente expôs.
IndeterminateResultException descartava a resposta que a motivou: o statusCode
chegava às fábricas, entrava na mensagem formatada e não virava dado; o corpo se
perdia por completo. Diagnosticar o defeito acima exigiu escrever um script,
subir no servidor de produção e refazer a chamada com o certificado — por fora do
SDK. A exceção passa a carregar statusCode, body (truncado em 8 KiB) e raw (o
JSON decodificado, sem truncar), com o que cada ponto de falha tem em escopo.
NfseFailed ganha o throwable, para que o listener alcance a mesma evidência sem
depender do catch do chamador.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@jonathanpmartins
jonathanpmartins merged commit d5389d6 into main Aug 4, 2026
10 of 11 checks passed
@jonathanpmartins
jonathanpmartins deleted the fix/consulta-eventos-envelope-real branch August 4, 2026 19:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant