Repository navigation
fix: ler o envelope de eventos que a SEFIN devolve - #6
Merged
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
O defeito
consultar()->eventos()exigiaeventoXmlGZipB64no topo da resposta. OSefinNacional_1.6.0devolve uma listaeventos[], com o XML emeventos[].arquivoXmle 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 viravaIndeterminateResultException.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 doGET. 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,eventoXmlGZipB64de topo depois.storage/prefeituras.jsontem prefeituras de implementação própria que podem cumprir o contrato declarado.EventsResponse::$eventos— lista deEventoConsultado(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:0x1festá fora do alfabeto base64, então o decode estrito de um conteúdo já descomprimido falha sozinho.IndeterminateResultExceptioncarregastatusCode,body(8 KiB) eraw.NfseFailedcarrega othrowable.executeRawdeclara 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?stringe estourarTypeError.Compatibilidade
Construtores públicos preservados:
$previouscontinua na posição 3 emIndeterminateResultException, ethrowableentrou por último emNfseFailed. A quebra é nas fábricas estáticas e emExecutesNfseRequests(porta@internal).Fora de escopo
ParsesEventResponse). A varredura em produção cobriu só o GET, e a rejeição E0822 que chegou por aquela rota indica que o POST devolveeventoXmlGZipB64mesmo. Só ganhou orawna exceção.GzipCompressorcobreDistribuicaoNSU.ArquivoXmlse ela existir, mas nada foi medido.Verificação
pest --coverage --min=100 --parallelpest --mutate --min=100 --parallelpest --type-coverage --min=100rector --dry-runphpstan analysepsalm --taint-analysispint -pDesign em
docs/superpowers/specs/2026-08-04-consulta-eventos-envelope-real-design.md.🤖 Generated with Claude Code