feat: hospitais do SUS - peçonhentos, oncologia e doenças raras - #891
Open
felipeflfranca wants to merge 12 commits into
Open
feat: hospitais do SUS - peçonhentos, oncologia e doenças raras#891felipeflfranca wants to merge 12 commits into
felipeflfranca wants to merge 12 commits into
Conversation
Expõe os hospitais de referência habilitados pelo SUS em três verticais — animais peçonhentos, oncologia e doenças raras — mais os centros CIATOX. São 2.342 estabelecimentos, 99,7% geocodificados. Os dados são extraídos de documentos públicos do Ministério da Saúde (PDFs estaduais e planilhas de habilitação) pelo MapaSUS, que os normaliza e geocodifica. Nem o MapaSUS nem a BrasilAPI são serviços oficiais do órgão — toda resposta traz `fonte.oficial: false` e o aviso de procedência. Rotas: - GET /hospitais/v1 filtros + paginação - GET /hospitais/v1/proximos busca por raio, ordenada por distância - GET /hospitais/v1/opcoes vocabulário aceito, com contagem - GET /hospitais/v1/ciatox centros de assistência toxicológica Snapshot em vez de proxy: o MapaSUS roda 100% em free tier e limita a 15 req/min. A BrasilAPI serve de lambdas efêmeras e não tem estado compartilhado para coordenar concorrência, então um pico de tráfego viraria uma rajada no Supabase deles. Nenhuma rota toca a rede — o dataset vem de services/hospitais/snapshots/latest.json, atualizado por GitHub Action diário que faz ~83 requests serializadas de um único IP. Mesmo padrão do snapshot de sedes de bancos, com guardrail de 80% para um sync quebrado não zerar o dataset. Busca unificada: `?atendimento=` atravessa as três verticais e aceita nome popular (cascavel, jararaca, armadeira), soro em PT ou EN, habilitação (radioterapia, cacon) e código de portaria (17.07, 35.16). Sem isso a API só responderia a quem já conhece o vocabulário em inglês. Termo desconhecido retorna 400 em vez de esconder um typo atrás de lista vazia. Os qualification_codes de doenças raras vêm como texto livre com ruído de OCR, então o casamento é por extração de código via regex, nunca por texto. Aviso de emergência em toda resposta: alguém pode consultar esta API no meio de um acidente. A orientação é ligar 192 (SAMU) e, em caso de acidente com peçonhento, ligar para o CIATOX antes de se deslocar — a unidade listada pode estar sem o soro. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`tratamento` e `habilitacao` resolviam contra o mesmo vocabulário que `atendimento` e devolviam resultados idênticos em todos os casos testados — só triplicavam o que o usuário precisa decidir antes da primeira chamada. Para restringir a uma vertical, `vertical=` já existe. Busca principal: 9 -> 7 parâmetros. /proximos: 11 -> 10. Também torna o merge do snapshot aditivo nos campos de lista (`verticals`, `treatments`, `specialties`). O espalhamento anterior deixava a última coleta sobrescrever o que as anteriores trouxeram. Não muda o dado hoje — os 5 hospitais com soro fora da vertical de peçonhentos vêm assim do MapaSUS, que não os retorna em /v1/venomous-animals/hospitals — mas evita perda silenciosa se o upstream passar a devolver `verticals` parcial por endpoint. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ixa de ser 500 A documentação descrevia o que cada rota retorna, não que pergunta ela responde. Agora abre com uma tabela de perguntas reais mapeadas para a requisição correspondente, explica para que serve cada uma das quatro rotas e o que distingue as três verticais. Todos os exemplos foram executados contra o servidor antes de entrar na doc. O parâmetro `atendimento` ganhou a tabela das quatro famílias de valor que aceita — nome popular do animal, nome do soro em PT ou EN, habilitação e código de portaria — e `vertical` passou a dizer que é opcional e raramente necessário. fix: /proximos devolvia 500 para qualquer falha de CEP, vazando o CepPromiseError cru. CEP malformado agora é 400 e CEP inexistente é 404, usando a mesma tradução que pages/api/cep/v2/[cep].js já fazia. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A revisão anterior tinha seções próprias de explicação — tabelas markdown, títulos e blocos de código na descrição da tag —, coisa que nenhum outro domínio do projeto faz. A tag ficou com 2.915 caracteres contra 316 da maior das outras, quase 10x fora do padrão. Reescrito na convenção da casa: tag em prosa curta com HTML (<b>, <code>, <abbr>), operações em prosa de uma a três frases, e o detalhe do vocabulário na descrição dos parâmetros — que é onde os outros domínios o colocam. Medido contra o teto real do projeto, em texto visível sem tags: tag 348 (maior das outras: 316, em ISBN) atendimento 323 (maior parâmetro das outras: 321, em cep) operações 244-344 (maior das outras: 423, em cptec) Também troca Itatiba por Campinas nos exemplos. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…a campo traz Faltavam duas coisas depois do ajuste ao padrão da casa. A tag dizia "hospitais de referência do SUS" sem nunca explicar o que isso significa. Agora abre pela definição, como a tag de ISBN faz com ISBN: são estabelecimentos que o Ministério da Saúde habilitou por portaria para um atendimento específico — a habilitação é oficial, não uma classificação nossa. E 8 dos 14 campos de resposta não tinham descrição, que é justamente onde o Redoc responde "o que dá para obter". Todos os campos de Hospital, CiatoxCentro e HospitalProximo agora descrevem o que trazem, incluindo duas ressalvas que faltavam: `address` pode vir incompleto, e `lat`/`lng` são geocodificados a partir do endereço — não vêm da fonte oficial e estão sujeitos a erro. A tag ficou em 390 caracteres visíveis contra 316 da maior das outras. O excedente é a definição do domínio e o aviso de emergência. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
"Encontra o hospital mais próximo que oferece um atendimento" dava a entender que a rota alcança qualquer hospital do país. Não alcança: a base cobre exclusivamente três habilitações do Ministério da Saúde. Os quatro summaries agora nomeiam o escopo, e as duas rotas de busca dizem no primeiro período que a base não é um cadastro geral de hospitais. Cabe no padrão: os summaries ficaram entre 64 e 88 caracteres, contra 108 do maior do projeto (cep v3). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… pública
Revisão de qualidade sobre a implementação. Quatro problemas reais.
Camadas invertidas: `query.js` — validação de entrada — importava de
`index.js`, o módulo que carrega 1,5 MB de snapshot, só para usar duas funções
puras. `VERTICAIS` e `resolverAtendimento` não tocam o dataset; são vocabulário
e foram para `vocabulario.js`. Agora `vocabulario.js` não tem nenhuma
dependência, `query.js` depende só dele, e `index.js` é o único que conhece o
snapshot.
Superfície pública inflada: dez exports em `vocabulario.js` e quatro constantes
exportadas em `index.js` e `query.js` que ninguém consumia de fora. Fechados —
`vocabulario.js` foi de dez exports para quatro.
Duplicação: `throw new BadRequestError({ message, type: 'validation_error' })`
aparecia seis vezes. Extraído para `invalido()`.
Coesão: `parseRaioEmMetros` era o único parser fora de `query.js`, definido
solto dentro do handler. Movido, junto com o `1000` mágico da conversão de
metros, agora `METROS_POR_KM`. `parseCoordenada(x, 'latitude', 90)` virou
`parseLatitude(x)`, tirando do handler o conhecimento dos limites.
`extrairCodigos` compilava uma RegExp nova a cada chamada, dentro do laço que
roda por hospital, e dependia de zerar `lastIndex` à mão entre chamadas de uma
regex global. As duas regex agora são compiladas uma vez e o casamento usa
`matchAll`, que não depende de `lastIndex`.
Comportamento inalterado: mesmas contagens em todas as buscas, 46 testes E2E
passando.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…idade
Segunda passada de qualidade, aplicando o que a primeira tinha deixado na mesa.
Origem sai do handler: buscarCep e resolverOrigem fazem chamadas externas
(cep-promise, geocoding) e viviam em pages/api — o CLAUDE.md reserva handlers
para roteamento e resposta, e chamadas externas para services. Foram para
services/hospitais/origem.js, e o handler de /proximos caiu de 146 para 43
linhas: resolve origem, valida raio, consulta, pagina, responde.
Paginação num só lugar: o fatiamento slice(offset, offset + limit) e o
envelope {total, limit, offset, items} estavam duplicados nas duas rotas de
busca. paginar() em query.js encapsula os dois; parsePaginacao virou interno.
Contrato explícito: getHospitaisProximos repassava ...filtros por spread, o
que aceitaria silenciosamente qualquer filtro futuro sem registro. Agora nomeia
vertical e atendimento.
Retry deduplicado no script de snapshot: a chamada axios idêntica aparecia
duas vezes no caminho do 429.
Testes de unidade: vocabulario.js ficou puro e sem dependências na passada
anterior, mas a extração de códigos sobre texto sujo de OCR — a lógica mais
frágil do domínio — só era coberta de longe pelos E2E. 11 testes cobrem
aliases, equivalência de grafias, precedência, extração multi-código com
quebra de linha e ponto ausente, e a autoconsistência de /opcoes. Mais 3 para
o haversine (distância conhecida SP–Rio, zero e simetria).
Camadas agora numa direção só: vocabulario (zero deps) <- query <- origem;
index.js é o único módulo que conhece o snapshot.
Comportamento inalterado: mesmas contagens e mesmos códigos de erro em todas
as rotas. 60 testes passando (46 E2E + 14 de unidade).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…napshot O script já chamava /v1/states e descartava tudo menos as siglas. Agora guarda o que o MapaSUS reporta por UF no momento da coleta — status, synced_at (última checagem da fonte) e updated_at (data publicada pelo gov.br) — em `sincronia_da_fonte` no metrics-latest.json. Permite auditar de quando são os dados de cada UF sem mudar o contrato da API. Também documenta explicitamente que a coleta é completa, não incremental: qualquer mudança de conteúdo na fonte entra no snapshot seguinte mesmo com o número de registros inalterado — o guardrail de 80% só bloqueia encolhimento abrupto, verificado por experimento com mock (mesmo tamanho, um nome corrigido: a correção foi promovida). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
@felipeflfranca is attempting to deploy a commit to the BrasilAPI Team on Vercel. A member of the Team first needs to authorize it. |
- npm ci --ignore-scripts no workflow de snapshot - node:fs / node:path nos requires - valida MAPASUS_BASE_URL do ambiente (parse http(s) + usa só a origem normalizada) e monta URLs de requisição com new URL - catches de readJsonFile e fetchStates registram o motivo do fallback - optional chaining no tratamento de 429 - for-of nos loops sequenciais (com disable justificado do no-restricted-syntax do Airbnb) - contagens derivadas da resposta HTTP passam por Number.parseInt antes de irem para logs e métricas; by_vertical só aceita slugs conhecidos - vocabulario: String.raw na regex de portarias e extrairCodigos sem retorno invariante (Blocker do Sonar) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fecha as duas lacunas apontadas na comparação com a API do MapaSUS:
- GET /hospitais/v1/cnes/{cnes}: localiza estabelecimento pelo
identificador oficial nacional (mais estável que o id interno da
fonte). Normaliza zeros à esquerda e separadores perdidos na extração
dos PDFs e devolve lista — o mesmo CNES pode aparecer em mais de um
registro quando a fonte não consolidou as verticais. 404 explica que
a cobertura é parcial.
- GET /hospitais/v1/estados: totais por UF + frescor reportado pela
fonte na coleta (status, synced_at, updated_at, do sincronia_da_fonte
do metrics) — em dado de saúde, 'de quando é isso?' importa tanto
quanto o dado.
Ambas servidas do snapshot em memória, com OpenAPI e E2E (10 testes
novos).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…snapshot A allowlist introduzida na rodada do Sonar usava os slugs da API em português (peconhentos, oncologia, raras), mas hospital.verticals traz as chaves de banco do MapaSUS (venomous_animals, oncology, rare_diseases) — o by_vertical das métricas vinha vazio. A allowlist agora deriva das chaves reais. Snapshot regenerado com a coleta de 12/08: 2342 hospitais (2336 geocodificados), 32 ciatox e sincronia_da_fonte preenchido para as 27 UFs — é o que alimenta o /hospitais/v1/estados. Co-Authored-By: Claude Fable 5 <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.



📋 Descrição
Novo domínio
hospitais: expõe os hospitais de referência habilitados pelo SUS em três verticais — animais peçonhentos (soros antiveneno), oncologia (habilitações CGCAN) e doenças raras (habilitações SRDR/SAE) — mais os 32 centros CIATOX de assistência toxicológica. São 2.342 estabelecimentos, 99,7% geocodificados.Esses dados não existem hoje em nenhuma API pública: as fontes originais são PDFs estaduais e planilhas XLSX no gov.br/saude. A utilidade é direta — achar o hospital com o soro antiveneno certo é um problema com janela de minutos.
Rotas
GET /api/hospitais/v1GET /api/hospitais/v1/proximosGET /api/hospitais/v1/opcoesGET /api/hospitais/v1/ciatoxBusca unificada
?atendimento=atravessa as três verticais e aceita nome popular do animal (cascavel,jararaca,escorpiao), soro em PT ou EN (crotalico/Crotalic), habilitação (radioterapia,cacon,terapia-genica) ou código de portaria (17.07,35.16). Acento, caixa, hífen e underscore são equivalentes. Termo desconhecido retorna 400 com a lista de valores aceitos — lista vazia esconderia um typo atrás de "nenhum hospital encontrado", o que num endpoint de saúde é pior que um erro explícito.Arquitetura: snapshot, não proxy
A fonte dos dados é o MapaSUS (Codar-Sistemas/hospitais-referencia-api, MIT), que faz scraping diário dos documentos do MS, extrai (incl. OCR com verificação de confiança), normaliza e geocodifica.
Nenhuma rota chama o MapaSUS em request-time. Ele roda 100% em free tier (Supabase + Vercel Hobby, 15 req/min por IP); a BrasilAPI serve de lambdas efêmeras sem estado compartilhado, então sob pico cada cold start viraria uma rajada no serviço. Em vez disso, o dataset é servido de
services/hospitais/snapshots/latest.json(1,4 MB — menor que oncmList.jsonde 2,85 MB já versionado), atualizado por GitHub Action diário que faz ~83 requests serializadas com pausa de 5s, de um único IP. Guardrail de promoção: o snapshot só é sobrescrito se a coleta trouxer ≥80% dos registros da anterior — um sync quebrado na fonte não zera o dataset. Mesmo padrão do snapshot de sedes de bancos (banks-headquarters-snapshot.yml), com uma correção: o workflow commita apenaslatest.jsonemetrics-latest.json, sem os arquivos datados por run.Procedência e segurança
Toda resposta traz dois objetos fixos:
fonte— comoficial: falsee aviso explícito de que nem o MapaSUS nem a BrasilAPI são serviços oficiais do Ministério da Saúde; os dados vêm de documentos públicos do órgão, extraídos automaticamente.emergencia— SAMU 192, Disque-Intoxicação 0800 722 6001 e a orientação de ligar ao CIATOX antes de se deslocar (a unidade listada pode estar sem o soro). Dois testes E2E garantem que nenhuma rota responde sem esses avisos.O campo
requires_verificationmarca registros extraídos por OCR com confiança baixa.🎯 Tipo de Mudança
Mudança 100% aditiva. Os únicos arquivos existentes tocados foram
package.json(script novosnapshot:hospitais) epages/docs/doc/basic_info.json(uma linha: HOSPITAIS no grupo Saúde).📚 Checklist de Documentação
/pages/docs/doc/docs/HOSPITAIS_MAPASUS.mdTodos os exemplos da doc foram executados contra o servidor antes de serem documentados.
🧪 Checklist de Testes
npm test)60 testes: 46 E2E (
tests/hospitais-v1.test.js) + 11 de unidade para o vocabulário (extração de códigos sobre texto sujo de OCR, aliases, equivalência de grafias) + 3 para o haversine. Falhas pré-existentes em cambio/fipe/diasuteis/pix foram reproduzidas idênticas namainlimpa (provedores externos instáveis) — não relacionadas a este PR.💻 Checklist de Código
npm run fixantes de commitarZero dependências novas — até o haversine foi escrito à mão (
util/haversine.js, 19 linhas). Camadas numa direção só:vocabulario.js(puro, zero deps) ←query.js←origem.js;index.jsé o único módulo que conhece o snapshot.🚀 Checklist de Performance e Custos
Request-time: filtro em memória sobre import estático (1–8ms medidos localmente, sem chamadas de rede no caminho quente),
app({ cache: 86400 }). Únicas chamadas externas em request-time: geocoding da origem no/proximos(reusalib/fetchGeocoordinateFromBrazilLocation.js, timeout 2s) efetchCepquando a origem é CEP. Carga sobre o MapaSUS: ~83 requests/dia do GitHub Action, ~51 MB/mês de egress (1% da cota free do Supabase).🔍 Como Testar
npm run devcurl "localhost:3000/api/hospitais/v1?atendimento=cascavel&uf=SP&limit=3"(termo leigo → soro crotálico)curl "localhost:3000/api/hospitais/v1/proximos?municipio=Campinas&uf=SP&atendimento=escorpiao&raio_km=30"— conferirdistancia_kmcrescentecurl "localhost:3000/api/hospitais/v1/opcoes"— todovalorlistado é aceito pela busca, com o mesmototalcurl "localhost:3000/api/hospitais/v1?atendimento=cascavell"→ 400 com orientaçãonpm test -- hospitais haversine→ 60 testeslocalhost:3000/docs, grupo Saúde📸 Screenshots (se aplicável)
N/A — sem mudanças de UI além do novo grupo no Redoc.
📎 Issues Relacionadas
Nenhuma issue aberta previamente. Se os mantenedores preferirem discutir a arquitetura em issue antes do review, abro uma com prazer.
📝 Notas Adicionais
Divulgação de conflito de interesse: o MapaSUS foi desenvolvido por mim (Codar Sistemas). Peço review e merge de outro mantenedor.