0050 — Consulta ao Boa Vista (Cadastro Positivo) na análise de crédito, configurável por tenant¶
| Status | CONCLUÍDA |
| Branch | spec/0050-analise-credito-boavista |
| Criada em | 2026-09-03 |
| Apps afetados | api · web · e2e |
| Revisão do problema | aprovada (rodada 1) |
| Revisão da experiência | aprovada (rodada 4) |
| Revisão do detalhamento | aprovada (rodada 3) |
| Revisão da implementação | aprovada (rodada 4, branch) |
Status:
RASCUNHO→APROVADA→EM IMPLEMENTAÇÃO→CONCLUÍDA
A spec tem três partes, escritas em ordem, e quatro portões — cada um revisado por um subagente de contexto limpo, em até 3 rodadas. A máquina de revisão é do plugin superpowers; a régua é a rubrica correspondente em .claude/rubrics/. Achados vêm classificados: qualquer Critical reprova, Important em aberto reprova salvo decisão registrada do usuário, e Minor vira pendência aceita.
- A Parte I diz qual é o problema e como se convive com ele hoje. Revisada na seção 6 (necessidade × jornada de hoje). É o portão mais barato de todos, e o único que pergunta contra que problema isto está sendo construído.
- A Parte II diz como se usa o que vai existir. Revisada na seção 8 (jornada × viewports).
- A Parte III diz como se constrói. Revisada na seção 16 (segurança × clareza técnica) — autorização, isolamento, e se sobrou decisão para quem vai codar.
- A implementação é medida contra as três, na seção 17 (aderência × qualidade × testes) — uma revisão por fase, e uma da branch inteira antes de fechar.
Cada parte só se escreve depois que a anterior passou. Descrever tela sobre problema mal enquadrado, ou detalhar endpoint sobre experiência vaga, é o jeito mais caro de descobrir que era outra coisa.
Parte I — O problema¶
Aqui não entra solução: nem tela, nem endpoint, nem botão. Só o que dói, para quem, e o que se faz hoje. Se você já sabe qual é a tela, guarde para a Parte II.
1. Dor¶
Hoje a decisão do analista numa Análise de Crédito se apoia em dois insumos só: a recomendação automática (credit_recommendation, calculada por compute_credit_summary pela regra 3× o Pacote Locatício sobre a renda declarada) e o campo de texto livre opinion, onde o analista registra o parecer técnico. Não existe nenhum dado de bureau de crédito estruturado no sistema — consulta a bureau foi marcada explicitamente como fora de escopo em três specs anteriores (0003, 0015, 0025).
Na prática, os analistas já preenchem essa lacuna por conta própria: saem do Garantora, abrem o Boa Vista (ou outro bureau) em outro site, digitam de novo o CPF do inquilino — que o Garantora já tem cadastrado —, leem o relatório (score, pendências financeiras, protestos, cheques sem fundo, renda presumida) e voltam para colar um resumo no campo opinion. Isso acontece regularmente, não como exceção.
O custo de conviver com isso, hoje:
- O dado vira texto solto. Um score numérico, o valor de uma pendência, a existência de um protesto — tudo isso é resumido de próprio punho e perde a estrutura. Não dá para comparar entre análises, nem para reler depois o número exato que foi visto.
- Nada garante que a consulta aconteça. Depende de o analista lembrar de sair do sistema, redigitar um CPF que o sistema já tinha, e voltar. Não há como saber, olhando a análise, se aquele passo foi cumprido.
- Não há trilha do que foi consultado. Se uma decisão for questionada depois — por que este inquilino foi aprovado, ou por que não — não existe registro de que a consulta ao bureau foi feita, quando, nem o que ela mostrava naquele momento.
- O tempo desse vai-e-vem é pago manualmente, em toda análise, por sair do sistema, digitar o CPF de novo, ler o relatório em outro lugar e resumir.
2. Quem sente¶
ANALYST, em toda Análise de Crédito que decide. É quem hoje sai do sistema para consultar o bureau e quem escreve o resumo emopinion— é o caminho já praticado regularmente, não uma exceção pontual.ADMIN_TENANT, indiretamente: é quem responde, perante a garantidora, pela consistência do critério de aprovação entre analistas diferentes — e hoje não tem como garantir que todos consultem o bureau da mesma forma, no mesmo momento, nem que o resultado fique registrado.
Não há operação hoje para medir volume — a prática de consultar o bureau é um passo manual fora do sistema, sem contagem alguma. O alcance é o que sustenta a dor: é caminho já percorrido em toda análise decidida, não um caso de borda.
3. A jornada de hoje¶
Passo a passo, sem a feature:
- Inquilino e demais participantes preenchem a Análise de Crédito (dados financeiros, renda declarada) e ela é submetida (
DRAFT→SUBMITTED→IN_ANALYSIS). - O sistema calcula automaticamente
credit_recommendation, comparando a renda declarada com o Pacote Locatício (aluguel + condomínio + IPTU) pela regra 3×. - A análise chega para o
ANALYSTdecidir. Antes de decidir, ele sai do Garantora, abre o site do Boa Vista (ou outro bureau) por fora, digita à mão o CPF do inquilino — que já está cadastrado no Garantora — e lê o relatório: score, classificação de risco, pendências financeiras, protestos, cheques sem fundo, renda presumida. - Ele volta ao Garantora e resume o que viu, escrevendo de próprio punho no campo
opinion— texto livre, sem os valores exatos do relatório. - A decisão (aprovar, pendenciar, rejeitar) é registrada com base na regra 3× mais esse resumo textual.
Onde quebra: nos passos 3 e 4 — a saída do sistema, a redigitação de um dado que o Garantora já tinha, e a perda de estrutura do dado ao virar resumo em texto livre. Nada no sistema hoje sinaliza se esse passo foi cumprido ou pulado numa análise específica.
O que já se tentou: a própria prática manual descrita acima já é o contorno em uso — não existe outro workaround (planilha paralela, campo reaproveitado) além de "o analista faz por fora e resume no parecer".
4. Resultado esperado¶
A consulta ao Boa Vista (produto Cadastro Positivo, via API Full) passa a acontecer dentro do Garantora, automaticamente, ao submeter a análise — nos tenants que tiverem essa configuração ativada. O analista deixa de sair do sistema e de redigitar o CPF.
O resultado volta estruturado (score, classificação, probabilidade de inadimplência, pendências financeiras, protestos, cheques sem fundo, renda presumida) e fica registrado junto à análise — visível para o analista no momento de decidir, e consultável depois, sem depender de o analista ter copiado algo para o parecer.
Como se sabe que resolveu: toda Análise de Crédito de um tenant que ativou a configuração passa a ter o resultado da consulta registrado automaticamente, sem exceção e sem depender de o analista lembrar — o passo que hoje é manual, opcional na prática e sem registro vira automático, uniforme e auditável.
5. Fora de escopo¶
- Outros bureaus (Serasa, SPC, Bacen SCR) — a API Full oferece produtos para os três, mas esta spec cobre só o Boa Vista / Cadastro Positivo. Fica para spec futura.
- Consulta de CNPJ / inquilino pessoa jurídica — só CPF de pessoa física entra agora.
- O resultado não entra no cálculo automático de
credit_recommendation. Continua sendo só a regra 3× o Pacote Locatício; o retorno do Boa Vista é informativo para oANALYSTdecidir — não substitui, nem altera, a recomendação automática. - Nenhum fluxo de consentimento novo. A consulta é coberta pelo aceite geral que o inquilino já dá ao contratar a garantia; esta spec não cria termo específico.
- Conta compartilhada da plataforma. A credencial (Bearer Token da API Full) é por tenant — cada garantidora traz a própria conta e o próprio saldo. Esta spec não cobre uma conta única da Garantora.
- Nova tentativa automática (retry) além da consulta feita na submissão. Se o provedor falhar (indisponibilidade, timeout, retorno incompleto), a análise segue sem o dado, com aviso — não há reprocessamento automático fora de escopo desta spec.
6. Revisão do problema¶
Preenchido pelo laço do /spec new, rubrica .claude/rubrics/problema.md. Registrar todas as rodadas, inclusive as reprovadas.
| Rodada | Critical | Important | Minor | Veredicto |
|---|---|---|---|---|
| 1 | 0 | 0 | 0 | Aprovada |
Resultado: aprovada na rodada 1.
Pendências aceitas: nenhuma.
Parte II — A experiência¶
Não preencher antes da revisão da seção 6 estar aprovada.
7. Jornada pretendida¶
- Ponto de entrada: não há um ponto de entrada separado. O gatilho é a própria submissão da Análise de Crédito, que já é o passo que o
ANALYST/participantes fazem hoje — não existe tela, menu ou ação nova para "consultar o bureau". - Jornada, caso feliz:
- Quem opera a análise (tipicamente
OPERATORou oAGENCY_USERda imobiliária) preenche e submete a Análise de Crédito com os dados do inquilino, como já faz hoje — o inquilino não acessa o sistema nem submete nada. - No mesmo momento da submissão, se o tenant tiver a configuração ativada, o sistema consulta o Boa Vista (Cadastro Positivo) usando o CPF do inquilino principal, que já está no cadastro — sem pedir nada a ninguém.
- Quando o
ANALYSTabre a análise para decidir, o resultado já está lá: um painel novo, ao lado do que já existe hoje (o que mostra a renda declarada e a recomendação automática), mostrando o que o Boa Vista retornou. - O
ANALYSTlê o painel novo junto com o que já lia antes, e registra o parecer da mesma forma que já faz — só que agora sem precisar sair do sistema para consultar nada por fora. - Custo de uso: nada digitado pelo usuário. O único dado que a consulta usa (o CPF do inquilino) já está cadastrado; ninguém volta a informá-lo.
7.1 Desktop¶
O painel novo entra na mesma coluna dos demais painéis de leitura da tela de Análise de Crédito, logo depois do painel que hoje mostra "o que o sistema sabe" (renda declarada e recomendação automática) e antes do painel de documentos pendentes — a mesma lógica de ordem que a tela já segue: primeiro o que embasa a decisão, depois o que falta. Segue o mesmo molde visual dos painéis existentes da tela: fundo do cabeçalho tingido pelo veredito, e um selo com ícone e texto no canto direito do cabeçalho (a mesma peça que hoje mostra "DENTRO"/"3 PENDÊNCIAS"/"COMPLETO" nos painéis existentes) — aqui, algo como "SEM RESTRIÇÃO" ou "N RESTRIÇÕES ENCONTRADAS". Sem ação nenhuma nele, é só leitura.
O corpo mostra, em linhas de rótulo/valor: score e classificação, probabilidade de inadimplência, pendências financeiras (quantidade e valor total), protestos (quantidade), cheques sem fundo (quantidade) e renda presumida (faixa).
A cor do cabeçalho reflete só as três restrições objetivas — pendência financeira, protesto e cheque sem fundo: vermelho se qualquer uma delas tiver ocorrência, verde se nenhuma tiver (nada encontrado é lido como veredito positivo, mesma leitura que o painel existente já dá para "dentro da política"). Score, classificação e renda presumida aparecem no corpo como dado, mas não entram na cor do cabeçalho — o Boa Vista já resume risco num score e numa probabilidade numérica, e não há, hoje, uma faixa de corte definida para tratar "score baixo sem nenhuma restrição objetiva" como alerta; decidir esse corte é análise de crédito, papel do ANALYST, não do painel.
7.2 Mobile (375px)¶
Mesmo padrão dos demais painéis desta tela: sempre aberto, sem colapsar. Nenhum painel de leitura da tela de Análise de Crédito hoje vira cartão colapsável — todos têm só o padding reduzido no mobile, mantendo o painel inteiro visível, empilhado na ordem vertical junto dos demais. O painel novo segue essa mesma regra, não a de outras telas do sistema que usam cartão colapsável para blocos de detalhe — aqui o padrão local é diferente, e é ele que vale.
No mobile, os mesmos campos aparecem, na mesma disposição rótulo-à-esquerda/valor-à- -direita de cada linha que os demais painéis já usam em qualquer largura — não há mudança de layout dentro da linha, só o padding do painel que diminui. Nenhum campo é escondido.
7.3 Estados¶
- Tenant sem a configuração ativada: o painel não aparece — não é um estado vazio dentro do painel, é a ausência do painel inteiro, igual a qualquer outro bloco condicional da tela.
- Análise ainda em
DRAFT(antes da primeira submissão), mesmo com a configuração ativada: o painel também não aparece — a consulta só acontece na submissão (seção 4), então antes dela não há o que mostrar. É o mesmo comportamento do caso "tenant sem configuração": o painel só entra na tela depois que a análise é submetida pela primeira vez. - Carregando (
PENDING, RN4b — correção pós-aprovação): a consulta roda em background, não mais dentro da submissão, então há uma janela real em que o painel existe mas não tem resultado ainda. Mostra um selo "consultando" (com indicador de progresso) e uma frase dizendo que o resultado aparece assim que terminar — sem exigir recarregar a página.apps/webfaz polling da análise a cada 3s enquanto durar, até um teto de 60s. Quem abre a tela um pouco depois da submissão normalmente nem vê este estado — o worker costuma responder em segundos —, mas ele existe e é real. - Consulta falhou (Boa Vista fora do ar, timeout, ou retorno incompleto/inconclusivo): o painel mostra que a consulta não pôde ser concluída, com uma mensagem genérica, sem código HTTP nem nome de fornecedor ("não foi possível concluir a consulta automática de crédito no momento da submissão" — texto ajustado depois da aprovação: a primeira versão citava "o Boa Vista" e o motivo técnico direto na tela, achado do usuário testando ao vivo — ver seção 18). Não há ação de tentar de novo dentro do painel — está fora de escopo, junto com qualquer nova tentativa automática (seção 5). O
ANALYSTsegue decidindo com o que tem disponível — a regra automática e os demais painéis —, exatamente como faz hoje, numa análise em que ainda não existisse este painel: a ausência do dado do Boa Vista não bloqueia a decisão. - Sem permissão: o painel não introduz uma permissão própria — aparece para quem já pode ver a Análise de Crédito hoje, e não aparece para quem não pode, seguindo a mesma regra de acesso que os demais dados da análise (renda declarada, documentos). Isso inclui o
AGENCY_USERda imobiliária parceira, que hoje já lê a análise: decisão explícita desta spec é que o painel do Boa Vista não segue a mesma restrição do painel de parecer interno (escondido da imobiliária) — o resultado do bureau é tratado como dado da análise, não como deliberação interna da garantidora.
8. Revisão da experiência¶
Preenchido pelo laço do /spec new, rubrica .claude/rubrics/experiencia.md. Registrar todas as rodadas, inclusive as reprovadas.
| Rodada | Critical | Important | Minor | Nota | Veredicto |
|---|---|---|---|---|---|
| 1 | 0 | 3 | 0 | — | Reprovada |
| 2 | 0 | 3 | 0 | — | Reprovada |
| 3 | 0 | 1 | 1 | — | Reprovada |
| 4 | 0 | 0 | 1 | 9 | Aprovada |
Resultado: aprovada na rodada 4.
Pendências aceitas: nenhuma — o único achado Minor da rodada 4 (menção ao nome do componente DecisionPanel na seção 7.3) foi corrigido no texto antes de fechar o portão.
Parte III — Detalhamento¶
Não preencher antes das revisões das seções 6 e 8 estarem aprovadas.
9. Regras de negócio¶
- RN1.
TenantSettingsganha um campocredit_bureau_type— enumNONE|BOAVISTA_CADASTRO_POSITIVO, defaultNONE— que é o "tipo de análise de crédito" que oADMIN_TENANTescolhe. Mesmo padrão de campo já usado emTenantSettingsparaInitialFeeType/InitialFeeBase(enum Python,native_enum=False, mapeado comoStringno banco). - RN2. Quando
credit_bureau_typeéBOAVISTA_CADASTRO_POSITIVO,TenantSettingsprecisa também de um Bearer Token da API Full (credit_bureau_token). Salvarcredit_bureau_type != NONEsem um token configurado (nem no payload da requisição, nem já salvo antes) é erro de validação — a configuração não é aceita pela metade. - RN3. O token é gravado criptografado em repouso com
Fernet(cryptography.fernet), chaveado porTENANT_SECRETS_KEY— uma variável de ambiente nova, no formato queFernet.generate_key()produz (32 bytes url-safe base64; não um texto livre como o default"change-me"deAPP_SECRET_KEY, que quebraria a inicialização doFernet). SemTENANT_SECRETS_KEYconfigurada, a aplicação falha ao subir — não há default inseguro para este campo. Rotação de chave está fora de escopo desta spec: trocarTENANT_SECRETS_KEYinvalidaria todos os tokens já cifrados, e não há, aqui, um mecanismo de recifragem em lote. O token nunca é devolvido em texto claro por nenhum endpoint — a leitura deTenantSettingsexpõe só um booleano (credit_bureau_token_configured), nunca o valor. É write-only: oADMIN_TENANTpode substituir o token, nunca reler o anterior. - RN4. A consulta ao Boa Vista (produto "Boa Vista Essencial Positivo", endpoint
scpc-boavistada API Full — corrigido depois da aprovação: a primeira versão desta RN citavacp-cadastrais-score-dividas, endpoint que não existe para a conta usada; a requisição real salva na coleção da API Full aponta parascpc-boavista, confirmado contra uma chamada real) é agendada automaticamente na submissão da Análise de Crédito (transiçãoDRAFT→SUBMITTED), depois que a transição de status já foi aplicada nesta mesma transação — usa o CPF do inquilino principal, já cadastrado. Comosubmit()é uma única transação de banco (o commit é doCommitRoute, ao final da requisição — RN7 detalha o que acontece se algo nesse bloco falhar), e como a transiçãoDRAFT→SUBMITTEDsó acontece uma vez no ciclo de vida de uma análise (depois disso ela transita só entreIN_ANALYSIS/PENDING— nome atual do status, renomeado dePENDING_DOCSna spec 0037 —, nunca de volta paraSUBMITTED), o agendamento acontece uma única vez por análise — não há reenvio nem nova tentativa em transições posteriores. - RN4b — a chamada de verdade roda em background, não dentro de
submit(). Correção pós-aprovação (achado do usuário testando ao vivo: "a consulta demorou para responder, gastou o crédito e não recebi o retorno, e ainda travou a tela"). Até esta correção, RN4 fazia a chamada HTTP à API Full dentro da transação da submissão — a resposta desubmit()só voltava depois da consulta terminar (até 10s de leitura), e uma conexão que caísse no meio deixava a chamada paga em curso no servidor sem que o resultado nunca chegasse a quem esperava.
O desenho novo: submit() grava um CreditBureauInquiry em status=PENDING (com requested_at, sem queried_at) e agenda um job numa fila (arq, Redis — infra nova, container worker em infra/*/docker-compose.yml, mesma imagem da api com o comando trocado). A resposta de submit() volta imediatamente, sem esperar a chamada HTTP. Um processo separado (app.worker, consumindo a fila) roda a chamada de verdade e, ao terminar, atualiza o registro para SUCCESS/FAILED (com queried_at) e grava a entrada de auditoria (RN7 mantém a garantia — ver abaixo). apps/web faz polling de GET /credit-analyses/{id} a cada 3s enquanto bureau_inquiry.status === "PENDING" (até um teto de 60s, depois do qual desiste silenciosamente — quem quiser o resultado atualiza a página), e o painel mostra um estado "consultando" nesse meio-tempo (seção 7.3).
Por que fila dedicada (Redis/arq), e não BackgroundTasks do FastAPI. A primeira opção cogitada foi BackgroundTasks — sem infra nova, roda no mesmo processo da API. Decisão do usuário foi por uma fila dedicada: sobrevive a um restart/deploy da API no meio de uma consulta em curso, e não compete por CPU/memória com quem serve requisição. Dentro da família "fila dedicada", arq (não Celery/RQ) por ser nativamente assíncrono — o resto do backend já é asyncpg/SQLAlchemy async/httpx async, e Celery/RQ exigiriam rodar código async dentro de worker síncrono.
Sem retry automático (decisão do usuário): uma falha na Boa Vista fecha FAILED de uma vez — repetir automaticamente dobraria o crédito gasto numa consulta que falhou por motivo real (CPF inválido, por exemplo), não só por instabilidade transitória. - RN5. A consulta só roda para inquilino pessoa física (CPF). Análise cujo inquilino principal é pessoa jurídica (CNPJ) não dispara consulta nem gera o painel — fora de escopo (seção 5). - RN6. O resultado é gravado numa entidade nova, CreditBureauInquiry, 1:1 com CreditAnalysis (mesmo padrão de Decision: FK única, back_populates nos dois lados). Guarda: status da consulta (PENDING/SUCCESS/FAILED — RN4b), os campos estruturados usados no painel (score, classificação, probabilidade de inadimplência, pendências financeiras — quantidade e valor —, protestos — quantidade —, cheques sem fundo — quantidade —, renda presumida — faixa), o payload bruto da resposta (JSON, para auditoria), o motivo quando FAILED, o instante em que a submissão agendou o job (requested_at, sempre presente) e o instante em que o worker terminou a chamada (queried_at, null enquanto PENDING). - RN7. Nenhuma falha deste bloco pode reverter a transição de status já aplicada na mesma transação, nem deixar um registro PENDING para sempre — o segundo virou explícito com RN4b: um "consultando" que nunca termina é o próprio sintoma que motivou mover a consulta para background. Isso cobre quatro problemas distintos: - Falha ao agendar o job (Redis fora do ar): _schedule_credit_bureau_inquiry fecha o registro como FAILED na hora ("Não foi possível agendar a consulta automática"), em vez de deixá-lo PENDING esperando um job que não existe. - Falha ao gravar o registro PENDING (erro de integridade, indisponibilidade do banco no momento do flush, dentro de submit()): a gravação acontece dentro de um session.begin_nested() (SAVEPOINT) — mesmo padrão já usado no repositório para isolar um flush que pode falhar sem envenenar a transação inteira (user_version_read_repo.py, numbering_sequence_repo.py). Se o INSERT falhar, só o SAVEPOINT é desfeito; a sessão continua utilizável, o commit() do CommitRoute grava a transição de status normalmente, e — novo em RN4b — o job não é agendado (não há registro para o worker atualizar). - Falha na consulta em si (timeout, indisponibilidade, HTTP diferente de 200, token inválido, resposta sem os campos esperados), dentro do worker: o client captura qualquer erro e devolve um resultado FAILED (motivo incluído) — nunca deixa a exceção subir. É o primeiro client HTTP externo do repositório com essa regra; o único precedente hoje (cep_service.py) sempre propaga a falha como exceção, e não serve de molde aqui. - Falha inesperada dentro do worker (bug, elegibilidade mudando entre a submissão e o job rodar — consulta desativada, inquilino trocado, análise removida): a parte de risco roda dentro de outro SAVEPOINT (mesmo padrão); qualquer exceção fecha o registro como FAILED com um motivo (específico quando é elegibilidade mudando, genérico quando é inesperado de verdade) — nunca deixa o registro preso em PENDING. - RN8. O resultado é só informativo. Não participa do cálculo de credit_recommendation (compute_credit_summary, regra 3× o Pacote Locatício) — essa regra automática não muda. - RN9 — o resultado vive no locatário, não na análise; e há uma trava contra consultar de novo. Segunda correção pós-aprovação (achado do usuário: "quero que todas essas informações estejam associadas ao inquilino [...] Coloque também uma trava que somente chamar a api para verificar o crédito se não tiver feito antes").
Renter ganha credit_bureau_status (RN4b), credit_bureau_data (JSON — pedido do usuário dizia "jsonb"; implementado como JSON genérico do SQLAlchemy, não o tipo JSONB do Postgres, para rodar igual em SQLite e Postgres sem exigir Docker no pytest — mesma convenção já documentada no CLAUDE.md da API), credit_bureau_ requested_at e credit_bureau_queried_at. A tabela credit_bureau_inquiries (RN6, 1:1 com CreditAnalysis) deixa de existir — não há base instalada a preservar (CLAUDE.md, "Estágio do produto"), e manter as duas fontes vivas duplicaria o dado. CreditAnalysisResponse.bureau_inquiry passa a ser derivado: lido do locatário principal (LESSEE, is_primary=true) da análise, na hora de montar a resposta — nenhuma FK própria.
A trava: a submissão só agenda uma nova consulta se o locatário nunca teve um resultado SUCCESS. Um FAILED anterior não trava — uma falha transitória não deveria deixar o inquilino sem dado até alguém lembrar de forçar manualmente — nem um PENDING em andamento é duplicado (evita dois jobs concorrentes gastando duas consultas pelo mesmo locatário). O mesmo inquilino pode aparecer em várias análises ao longo do tempo (mais de uma tentativa de locação, por exemplo); antes desta correção, cada nova análise dispararia uma nova consulta paga — a trava fecha exatamente esse gasto duplicado.
"Reanalisar" — só para a garantidora. Pedido do usuário: "um botão de reanalisar mas somente para garantidora". Endpoint novo, POST /renters/{id}/reanalyze-credit- bureau, ignora a trava de propósito (é a única forma de conseguir uma nova consulta depois de um SUCCESS), exige a permissão nova renters:reanalyze_credit_bureau — AGENCY_USER não a tem (seção 10.1). Recusa com 422 CREDIT_BUREAU_NOT_ACTIVE (tenant sem a consulta ativada) ou 422 CREDIT_BUREAU_RENTER_NOT_ELIGIBLE (locatário PJ ou sem CPF) — os mesmos dois motivos que já impediriam o agendamento automático, mas ditos em voz alta em vez de um 200 silencioso que não mudaria nada. - RN9b — modo sandbox, para testar sem gastar crédito real. Correção pós-aprovação, pedido do usuário: "quero ter uma opção de ativar no dev-remote esse fluxo para fechar a funcionalidade da interface, então deixar uma configuração no env" — depois de eu ter tentado uma verificação manual pontual em dev-remote (login com credencial real via curl para criar dado de teste) e o classificador de modo automático do Claude Code bloquear a ação por ser sensível demais para automatizar; em vez de insistir por outro caminho, o pedido virou este item permanente da spec.
CREDIT_BUREAU_SANDBOX_MODE (booleano, app/core/config.py, default False) — ligado, query_boavista_cadastro_positivo não chama a API Full: lê app/services/credit_bureau_sandbox/boavista_cadastro_positivo_sucesso.json (o mesmo retorno completo e real do Apêndice A, fictício e confirmado pelo usuário) e processa pelo mesmo _extrair() que a chamada de verdade usa — a única coisa pulada é o httpx.AsyncClient.post. Toda consulta, com qualquer token e qualquer CPF, devolve o mesmo resultado fixo (SUCCESS, score 148, classe E).
Trava de segurança: create_app() (app/main.py) recusa subir se CREDIT_BUREAU_SANDBOX_MODE estiver True junto com APP_ENV=production — mesmo espírito de TENANT_SECRETS_KEY sem default inseguro (RN3). Um resultado fictício, indistinguível de um real, vazando para produção enganaria quem decide uma análise de crédito de verdade — por isso a checagem falha o boot, não só loga um aviso. A auditoria (AuditLog, action="BUREAU_INQUIRY") marca o resultado com o prefixo [SANDBOX — resultado fictício, sem consulta real] na descrição e sandbox: true no payload, para nunca ser lida como uma consulta de verdade mais tarde.
Ligado só no .env do dev-remote (infra/dev-remote/.env) — nunca no .env.example de produção, que documenta a variável com o aviso e o default false. - RN9c — a tela de Parâmetros mostra o modo sandbox, e dispensa o token enquanto ele estiver ligado. Achado do usuário testando ao vivo, depois do RN9b: "quando estou entrando na tela de parâmetros se está com sandbox ativo não tem opção de colocar o token e informa que está ativo o sandbox com evidência." Sem isto, PATCH /settings continuava recusando credit_bureau_type != NONE sem token (RN2) mesmo num ambiente onde o token nunca é usado de verdade — a garantidora seria obrigada a colar um valor qualquer só para satisfazer uma validação sem propósito ali.
TenantSettingsResponse ganha credit_bureau_sandbox_active: bool — não é dado do tenant, vem de app_settings.CREDIT_BUREAU_SANDBOX_MODE (mesmo padrão computado de credit_bureau_token_configured, sempre sobrescrito pelo service antes de a resposta sair). RN2 passa a ter uma terceira condição: o token só é exigido se o modo sandbox não estiver ligado — fora do dev-remote (ou com a variável desligada), a validação original continua intacta.
apps/web: com credit_bureau_sandbox_active === true, a tela de Parâmetros não renderiza o campo de Bearer Token — no lugar dele, um Alert explícito diz que o modo sandbox está ativo, que toda consulta devolve um resultado fictício sem custo, e que o modo nunca fica ativo em produção. É a "evidência" pedida: sempre visível enquanto o tipo de análise estiver diferente de NONE, não escondida atrás de tooltip ou aba.
10. Autorização e isolamento¶
10.1 Matriz de papéis¶
| Papel | Pode | Não pode |
|---|---|---|
ADMIN_GLOBAL | O mesmo acesso que já tem hoje à Análise de Crédito (herda todas as permissões) | Esta spec não muda o acesso do ADMIN_GLOBAL a dado de negócio do cliente — nenhuma tela nova em /app/admin/* |
ADMIN_TENANT | Configurar credit_bureau_type/token em Configurações (já é require_system_role("ADMIN_TENANT")); ler o painel do bureau em qualquer análise ou locatário do próprio tenant; forçar reanálise (renters:reanalyze_credit_bureau) | Reler o token já salvo em texto claro |
ANALYST | Ler o painel do bureau (identidade — seção 10.4); forçar reanálise | Configurar credit_bureau_type/token (não tem require_system_role("ADMIN_TENANT")) |
OPERATOR | Ler o painel; submeter a análise (credit_analyses:submit), o que agenda a consulta; forçar reanálise | Configurar credit_bureau_type/token |
VIEWER | Ler o painel (credit_analyses:read/renters:read) | Submeter análise, configurar tenant settings, forçar reanálise (renters:reanalyze_credit_bureau — papel só-leitura, mesmo padrão de nunca ganhar ação de escrita em nenhum outro recurso) |
AGENCY_USER | — | Ler o painel do bureau, em nenhuma das duas telas (correção pós-aprovação — reversão explícita da decisão original desta seção, que dava à imobiliária dona da análise o mesmo acesso da garantidora — CA8 antiga); forçar reanálise; ler/configurar o token; ler o painel de análise ou locatário de outra imobiliária (404, filtro já existente) |
O negativo mais relevante desta spec: nenhum papel — nem o ADMIN_TENANT — consegue ler o token depois de salvo. É write-only para todo mundo, sem exceção de papel.
10.2 Permissões¶
Uma permissão resource:action nova: renters:reanalyze_credit_bureau (RN9) — gate do endpoint POST /renters/{id}/reanalyze-credit-bureau, concedida a ADMIN_TENANT, ANALYST e OPERATOR; não a VIEWER (papel só-leitura) nem a AGENCY_USER (correção pós-aprovação: "somente a garantidora").
Ver o resultado do bureau não ganhou permissão própria — decisão deliberada, não esquecimento. É identidade de quem pergunta (partner_agency_id), o mesmo padrão já usado no isolamento por imobiliária (within_agency/agency_scope, seção 10.4) para todo o resto do cadastro do locatário — uma permissão RBAC exigiria o meta-teste test_toda_permissao_definida_e_exigida achar onde ela é exigida por um if descoberto pela varredura, e redação condicional de campo (não um bloqueio de requisição) não se encaixa no padrão que esse teste reconhece (require_permission no router, ou exigir_permissao no serviço) sem inventar um terceiro padrão só para isto.
As duas garantias que já existiam continuam:
- Leitura do painel (o resto): embutida na resposta de
GET /credit-analyses/{id}eGET /renters/{id}, atrás decredit_analyses:read/renters:read— sem gate adicional além da checagem de identidade acima. - Escrita de
credit_bureau_type/token: dentro doPATCHdeTenantSettings, atrás do mesmorequire_system_role("ADMIN_TENANT")que já protege todo o resto das configurações do tenant hoje — não há permissão granulartenant_settings:writeno RBAC (a escrita é 100% gate por papel de sistema), e esta spec não introduz uma.
10.3 Isolamento por garantidora (tenant)¶
Renter.credit_bureau_*(RN9) é sempre lido através deRenter, que já é filtrado portenant_idnoBaseRepository— nenhuma query nova que contorne esse filtro. O mesmo vale lendo pela análise:CreditAnalysisResponse.bureau_inquiryderiva do locatário principal, já carregado no escopo da análise (que também é filtrada por tenant).- A consulta ao Boa Vista usa o token do tenant do locatário (
renter.tenant_id) — no agendamento automático, resolvido a partir decredit_analysis.tenant_id(RN4); noreanalyze_credit_bureau, direto derenter.tenant_id, depois de_get_authorizedconfirmar que o locatário pertence ao tenant/imobiliária de quem chama — nunca aceito de parâmetro, header ou corpo da requisição. - Análise ou locatário de outro tenant → 404, nos dois endpoints que expõem o resultado (comportamento já existente para
credit_analyses/renters, herdado; esta spec não muda o código de erro) — inclusive noreanalyze-credit-bureau, que passa pelo mesmo_get_authorizeddo resto deRenterService.
10.4 Isolamento por imobiliária¶
AGENCY_USER não lê o resultado do bureau, em nenhuma das duas telas (RN9, correção pós-aprovação — reversão de CA8: a decisão original desta seção dava à imobiliária dona da análise o mesmo acesso da garantidora; o achado do usuário foi "somente a garantidora tem acesso a informação a imobiliária não tem"). A checagem é current_user. partner_agency_id is not None → None, no ponto único que monta cada resposta (_bureau_inquiry_for em CreditAnalysisService, _with_bureau em RenterService) — mesma família de checagem que within_agency/agency_scope já usam para o resto do isolamento por imobiliária, não uma permissão nova (seção 10.2 explica por quê).
O restante do isolamento por imobiliária no cadastro do locatário não muda: AGENCY_USER segue vendo (e só vendo) os locatários do próprio partner_agency_id — o filtro em GET /renters e GET /renters/{id} é o que já existia, e o campo credit_bureau some por cima dele, não no lugar dele. POST /renters/{id}/reanalyze-credit-bureau recusa com 403 para ela antes mesmo de chegar em _get_authorized — falta a permissão renters:reanalyze_credit_bureau, que o seed de RBAC nunca concede a esse papel.
10.5 Plataforma × cliente (SaaS admin)¶
Sem impacto. Esta spec não adiciona tela em /app/admin/*, nem endpoint cross-tenant. A configuração do tipo de análise e do token é feita dentro do próprio tenant (tela de Configurações), não pelo SaaS admin — e o ADMIN_GLOBAL não ganha nenhuma visão agregada de token ou resultado de bureau entre tenants.
11. Impacto por app¶
apps/api¶
- Repositories (padrão reaproveitado, nenhum arquivo alterado):
app/repositories/user_version_read_repo.pyeapp/repositories/numbering_sequence_repo.py— não são tocados por esta spec, mas são o precedente dosession.begin_nested()que RN7 exige replicar na persistência doCreditBureauInquiry. - Modelos:
TenantSettings(app/models/tenant_settings.py) ganha os camposcredit_bureau_type(Enum(CreditBureauType, name="credit_bureau_type", create_constraint=False, native_enum=False), defaultNONE) ecredit_bureau_token(String, guarda o texto cifrado,nullable=True). Novo enumCreditBureauType(str, enum.Enum)no mesmo arquivo, mesmo padrão deInitialFeeType.CreditBureauInquiry(novo model,app/models/credit_bureau_inquiry.py) — 1:1 comCreditAnalysis(FKcredit_analysis_id, índice único,back_populatesnos dois lados,lazy="noload", mesmo padrão deDecision). Novo enumCreditBureauInquiryStatus(SUCCESS/FAILED). Campos conforme RN6.CreditAnalysis(app/models/credit_analysis.py) — relação inversabureau_inquiry: Mapped["CreditBureauInquiry | None"].- Migrations Alembic (duas):
- Adiciona
credit_bureau_type(server_default='NONE', evita backfill) ecredit_bureau_token(nullable=True) atenant_settings.downgraderemove as duas colunas. - Cria a tabela
credit_bureau_inquiries, comFOREIGN KEY credit_analysis_id REFERENCES credit_analyses(id) ON DELETE RESTRICTe índice único emcredit_analysis_id— mesmoondeletequeDecisionjá usa para a FK decredit_analysis_id(decision.py:134), e mesmo padrão de índice único separado (nãounique=Truena coluna) usado na criação original deanalyses/decisions.downgradederruba a tabela. - Criptografia (novo, sem precedente no repositório):
app/core/crypto.pycomencrypt_secret/decrypt_secretusandocryptography.fernet.Fernet, chaveado por uma variável de ambiente nova —TENANT_SECRETS_KEY— emapp/core/config.pye.env.example(não reaproveitaAPP_SECRET_KEY, que hoje tem default inseguro"change-me"e serve a outro propósito).cryptographyentra como dependência nova empyproject.toml. - Service novo:
app/services/credit_bureau_service.py— client HTTP paraPOST https://api.apifull.com.br/api/scpc-boavista,Authorization: Bearer <token decifrado>, corpo{"document": "<cpf>", "link": "scpc-boavista"}— o campolinké obrigatório e, apesar do nome, não é webhook nem URL: é um identificador fixo do produto, igual ao segmento final da própria URL (confirmado contra a requisição real salva na coleção Postman da API Full e uma chamada real). Timeouts explícitos (mesmo estilo decep_service.py). Nunca lança — sempre devolve um resultadoSUCCESS/FAILED(RN7). tenant_settings_service.py: validação nova emupdate()— RN2 (tipo ≠NONEexige token configurado, no payload ou já salvo).credit_analysis_service.py:submit()— depois de_transition(...)confirmarSUBMITTED, setenant_settings.credit_bureau_type != NONEe o inquilino principal é pessoa física,_schedule_credit_bureau_inquirygrava umCreditBureauInquiryemPENDINGe agenda o job — sem chamarcredit_bureau_servicediretamente (RN4b).- RN4b — arquivos novos, correção pós-aprovação:
app/core/queue.py— poolarq/Redis (lazy,get_pool()) eenqueue_credit_bureau_inquiry().app/worker.py—WorkerSettingsdoarqe o jobrun_credit_bureau_inquiry_job, que abre sessão própria (AsyncSessionLocal) e delega paracredit_bureau_worker_service.complete_credit_bureau_inquiry.app/services/credit_bureau_worker_service.py(novo) — a metade da chamada de verdade: acha o registroPENDINGpela análise, resolve settings/renter frescos, chamacredit_bureau_service, fecha o registro (SUCCESS/FAILED) e grava a auditoria — tudo dentro de umSAVEPOINT, comexcept Exceptionexterno que garanteFAILEDmesmo num caminho totalmente inesperado (RN7).app/core/config.py—REDIS_URL(com default, não é segredo — mesmo padrão deDATABASE_URL).pyproject.toml/uv.lock— dependência novaarq(trazredis/hiredisjunto).alembic/versions/..._credit_bureau_inquiry_background.py—PENDINGno enum, colunarequested_at(not null),queried_atvira nullable.infra/{local,dev-remote,prd}/docker-compose.yml— serviçoredis(imagem oficial, semportsfora do local) e serviçoworker(mesma imagem daapi,commandtrocado paraarq app.worker.WorkerSettings, sem rodar migration/seed — só aapifaz isso).apps/api/Makefile/Makefileda raiz — alvodev-worker/worker-devpara rodar o worker localmente com hot reload (arq --watch).- Schemas:
TenantSettingsCreate/Update:credit_bureau_type: CreditBureauType,credit_bureau_token: str | None(só entrada, write-only).TenantSettingsResponse:credit_bureau_type: CreditBureauType,credit_bureau_token_configured: bool— nunca o campocredit_bureau_token.CreditAnalysisResponse: campo novobureau_inquiry: CreditBureauInquiryResponse | None.- Docstrings + Swagger:
summary/description/response_descriptionno campo novo doPATCH /tenant-settingse na resposta deGET /credit-analyses/{id}; atualizarTAGS_METADATAemapp/main.pyse a descrição do grupo mudar; docstrings Google-style nos models/services novos (consumidas pelowiki/via Sphinx AutoAPI). - Testes:
crypto.py: round-trip cifra/decifra.credit_bureau_service.py: sucesso (200 com payload válido), timeout, erro HTTP, payload incompleto — todos devolvem resultadoFAILEDsem lançar (é o comportamento que RN7 exige, e o que não tem precedente a copiar).credit_analysis_service.submit(): criaCreditBureauInquirySUCCESSquando o tenant tem o tipo ativado e a consulta funciona; criaFAILEDquando a consulta falha, sem impedir a transição paraSUBMITTED; não cria nada quando o tipo éNONEou o inquilino é PJ; e — o teste que sustenta RN7 — quando a gravação doCreditBureauInquiryfalha (ex.:flushforçado a lançarIntegrityError), a análise ainda assim ficaSUBMITTEDao final da requisição, provando que obegin_nested()isolou a falha sem envenenar a transação inteira.tenant_settings_service.update(): rejeita tipo ≠NONEsem token (RN2).GET /tenant-settings: nunca devolvecredit_bureau_tokenem texto claro, sócredit_bureau_token_configured(RN3) — teste que falharia se alguém adicionasse o campo bruto ao schema de resposta por engano.- Negativos de autorização:
ANALYST/OPERATOR/VIEWER/AGENCY_USERrecebem 403 ao tentarPATCH /tenant-settingscomcredit_bureau_type(mesma checagem de papel já testada hoje para os demais campos, estendida);AGENCY_USERde outra imobiliária recebe 404 ao tentarGET /credit-analyses/{id}de análise alheia (comportamento já existente, cobrindo que o campo novo não abriu brecha). - RN9 — arquivos, correção pós-aprovação (a consulta virou característica do locatário, não da análise):
app/models/renter.py— ganhaCreditBureauInquiryStatus(movido decredit_bureau_inquiry.py, que deixa de existir) e as quatro colunascredit_bureau_status/credit_bureau_data/credit_bureau_requested_at/credit_bureau_queried_at.app/services/credit_bureau_schedule.py(novo) —schedule_bureau_inquiry()eScheduleOutcome, a decisão compartilhada de "agenda ou não" (a trava) usada tanto porCreditAnalysisService._schedule_credit_bureau_inquiry(force=False) quanto porRenterService.reanalyze_credit_bureau(force=True).app/services/credit_bureau_worker_service.py—complete_credit_bureau_inquirypassa a receberrenter_id, não maiscredit_analysis_id; a auditoria viraentity_type="renter".app/services/credit_bureau_service.py— dois novos:result_to_stored_dict()(serializaCreditBureauResultpara o formato decredit_bureau_data, convertendoDecimalparastr) erenter_bureau_response()(o inverso, monta o schema de resposta a partir doRenter).app/services/renter_service.py—reanalyze_credit_bureau()(novo) e_with_bureau()(preencheRenterResponse.credit_bureau, exceto paraAGENCY_USER— seção 10.4).app/services/credit_analysis_service.py—_bureau_inquiry_for()(novo, módulo), lê o locatário principal já carregado porget_bulk_by_credit_analyses, sem consulta extra;_build_response/list()passamcurrent_useradiante (precisam dele para a checagem de imobiliária).app/repositories/renter_repo.py—save_isolated()(novo, mesmo padrãoSAVEPOINTque existia emcredit_bureau_inquiry_repo.py, que deixa de existir).app/repositories/credit_analysis_repo.py—get_bureau_inquiries_bulk()removido (a tabela que ele lia não existe mais).app/schemas/credit_bureau.py(novo) —CreditBureauInquiryResponsesai deschemas/credit_analysis.pypara um módulo próprio, importado tanto porschemas/renter.py(RenterResponse.credit_bureau) quanto porschemas/credit_analysis.py(CreditAnalysisResponse.bureau_inquiry) — evita import circular entre os dois.app/db/rbac_seed.py— permissão novarenters:reanalyze_credit_bureau.app/api/v1/routers/renters.py— rota nova,POST /{renter_id}/reanalyze-credit- bureau.- Migration nova —
PENDING/SUCCESS/FAILEDreaproveitando o enum Postgres já criado porn6p8r0t2v4x5(create_type=False, sem recriar o tipo); adiciona as quatro colunas emrenters; derrubacredit_bureau_inquiries. - Testes novos:
tests/test_renter_credit_bureau.py(trava, worker no locatário,reanalyze_credit_bureau, visibilidade porpartner_agency_id) —tests/test_credit_analysis_bureau_inquiry.pyfoi reescrito para cobrir só o que é específico da análise (agenda no locatário certo, segunda análise do mesmo inquilino não reconsulta,ADMIN_GLOBAL,AGENCY_USERnunca vê). - RN9b — arquivos, correção pós-aprovação (modo sandbox):
app/core/config.py—CREDIT_BUREAU_SANDBOX_MODE: bool = False.app/main.py— emcreate_app(), antes de montarFastAPI(...): recusa subir (RuntimeError) se o modo sandbox estiver ligado junto comAPP_ENV=production.app/services/credit_bureau_service.py— no topo dequery_boavista_cadastro_positivo, antes da decifragem do token: se o modo está ligado, lê o JSON fixo do disco, processa por_extrair()e devolve — sem instanciarhttpx.AsyncClient.app/services/credit_bureau_worker_service.py— a auditoria gravasandbox: app_settings.CREDIT_BUREAU_SANDBOX_MODEnopayloade prefixa a descrição (o import usa o aliasapp_settings— ver o bug de colisão de nome no Registro, RN9b).app/services/credit_bureau_sandbox/(novo diretório) —boavista_cadastro_positivo_sucesso.json(movido detests/fixtures/credit_bureau/, mesmo arquivo do Apêndice A) eREADME.md(documenta os dois usos: modo sandbox em runtime e fixture dotest_sucesso_com_o_payload_real_da_api_full).apps/api/.env.example,infra/dev-remote/.env.example,infra/prd/.env.example— documentam a variável, com o aviso de nunca ligar em produção.infra/dev-remote/.env(não versionado) ganhouCREDIT_BUREAU_SANDBOX_MODE=true.- Testes novos em
tests/test_credit_bureau_service.py:test_modo_sandbox_nunca_chama_a_api_full(o_FakeAsyncClientnem é armado — se a chamada real acontecesse, o teste estouraria por falta de resposta configurada) etest_modo_sandbox_nao_sobe_em_producao(create_app()levantaRuntimeErrorcomAPP_ENV=productione o modo ligado). - RN9c — arquivos, correção pós-aprovação (a tela de Parâmetros mostra o sandbox):
app/schemas/tenant_settings.py—TenantSettingsResponse.credit_bureau_sandbox_ active: bool(computado, mesmo padrão decredit_bureau_token_configured).app/services/tenant_settings_service.py— importaapp.core.config.settingscom o aliasapp_settings(mesma disciplina do RN9b, depois do bug de colisão de nome);update()pula a exigência de token da RN2 quando o modo sandbox está ligado;_to_response()preenche o campo novo.tests/test_tenant_settings_credit_bureau.py— casos novostest_sandbox_inativo_por_padrao,test_sandbox_ativo_aparece_no_settingsetest_sandbox_ativo_dispensa_o_token.tests/conftest.py— a suíte inteira passa a fixarsettings.CREDIT_BUREAU_SANDBOX_MODE = Falseno import (mesmo padrão desettings.BCRYPT_ROUNDS = 4, já existente) — sem isto, todo teste rodado dentro do container do dev-remote herdaria otruereal do.envdaquele ambiente (RN9b) e testaria um comportamento diferente do que descreve. Foi como o bug apareceu: ver Registro.
apps/web¶
- Componente movido para
components/shared/(RN9, correção pós-aprovação):apps/web/src/components/shared/BureauInquiryPanel.tsx— antes vivia só empages/credit-analyses/components/, e passou a ser usado em duas telas (achado do usuário: "na análise de crédito e no inquilino adicionar um painel"). Mesmo molde visual (Paper, cabeçalho tingido + selo, corpo com linhas rótulo/valor). Não aparece quandobureauInquiryénull/undefined(tenant sem o tipo ativado, locatário nunca consultado, ou quem vê éAGENCY_USER— o backend nunca envia o campo para ela). EstadoPENDING(RN4b): selo "CONSULTANDO" comCircularProgress, corpo com uma frase, sem nenhuma linha de dado. testIdPrefix, nãodata-testidfixo: o componente recebe um prefixo (proptestIdPrefix) —credit-analysis-bureauna Análise de Crédito (mantém os nomes de antes da mudança para componente compartilhado, sem quebrar o e2e existente) erenter-bureauno Inquilino. O container vira${prefix}-panel, o selo${prefix}-badge, e as linhas de dado${prefix}-score/-pendencias/-protestos/-cheques/-renda-presumida.- Botão "Reanalisar" (RN9, novo): prop
onReanalyze—undefinedsome o botão inteiramente (não aparece desabilitado); cada página só passa a função quandocurrent_user.permissionsincluirenters:reanalyze_credit_bureau.data-testid${prefix}-reanalyze. Desabilitado enquantostatus === "PENDING"ou enquanto a própria chamada de reanálise está em voo (propreanalyzing). - Polling (RN4b): cada página que usa o painel tem seu próprio
useEffectque busca o próprio recurso (GET /credit-analyses/{id}ouGET /renters/{id}) a cada 3s enquanto o status forPENDING, até um teto de 60s — busca só o recurso em si, não histórico/imobiliária/decisão/documentos, para não repintar a tela inteira a cada tentativa. O polling não é responsabilidade do componente do painel. pages/credit-analyses/show.tsx: painel entre o cabeçalho de decisão ePolicyPanel(pedido do usuário, ainda de pé desde a correção anterior); lê obureau_inquiryda análise (que o backend deriva do locatário principal) e passa oiddesse locatário (primary.renter.id) para oonReanalyze.pages/renters/show.tsx: painel novo entre os dados cadastrais e oDocumentsPanel; lêperson.credit_bureaudireto (é o próprio locatário da tela).- Tela de Configurações (
TenantSettings, onboarding/settings): campoSelectnovo ("Tipo de análise de crédito": Nenhuma / Boa Vista — Cadastro Positivo) e um campo de token que só aparece quando o tipo não é "Nenhuma" — write-only: mostra "configurado"/"não configurado" quando já existe, nunca o valor salvo, comdata-testidseguindo a convenção já em uso na própria tela (settings-<campo>-input, ex.:settings-guarantee-rate-input):settings-credit-bureau-type-input,settings-credit-bureau-token-input,settings-credit-bureau-token-status. - RN9c — o mesmo bloco, com
credit_bureau_sandbox_active === true: o campo de token some por completo (unmountOnExitdoCollapseque já existia); no lugar, umAlert severity="info"(data-testid="settings-credit-bureau-sandbox-banner") diz que o modo sandbox está ativo, sem custo e sem chamada real, e que nunca fica ativo em produção.pages/settings/index.tsx—TenantSettings/FormStateganham o campo,settingsToForm()o lê doGET. Testes novos empages/settings/index.test.tsx: campo ausente e banner presente com o modo ativo, campo presente e banner ausente sem ele, e salvar sem token funciona com o modo ativo. - Sider/menu: sem impacto — não é módulo novo, é campo/painel dentro de telas já existentes.
- Responsividade & PWA: painel segue exatamente o padrão mobile descrito na seção 7.2 (sem colapsar, mesma disposição rótulo/valor, só padding menor), nas duas telas. Sem impacto de PWA — não é asset novo nem rota offline.
apps/e2e¶
- Specs: um novo (
analise-credito-boavista.spec.ts) cobrindo: com o tipo desativado, o painel não aparece (CA1); com token inválido de propósito, a submissão falha na consulta sem bloquear, o painel troca sozinho para "não concluída",AGENCY_USERvê o painel mas não o parecer interno (CA4/CA8/CA9). - RN4b (correção pós-aprovação): o spec espera o worker de verdade. Roda contra o container
workerdo dev-remote, que consome a mesma fila Redis da API — não há mock nem simulação da fila neste nível. A prova de que osubmit()não bloqueia mais é cronometrada (Date.now()antes/depois doPOST /submit,< 5s), não a observação de um estadoPENDINGtransitório — um worker rápido o bastante pode fechar o registro antes da tela terminar de carregar, e isso é RN4b funcionando, não uma falha do teste. apps/e2e/fixtures/limpeza.ts:credit_bureau_inquiriesentra na lista de limpeza antes decredit_analyses(FKON DELETE RESTRICT) — sem isso a faxina inteira quebra na primeira execução do spec novo.
12. Contrato de API¶
Um endpoint novo (RN9, correção pós-aprovação) — os demais contratos ganham campos.
PATCH /api/v1/tenant-settings
Authorization: Bearer <token>
{
"credit_bureau_type": "BOAVISTA_CADASTRO_POSITIVO",
"credit_bureau_token": "<bearer token da API Full, texto claro na entrada>"
}
credit_bureau_token nunca aparece no corpo de resposta — nem aqui, nem em GET /api/v1/tenant-settings.
Erro de negócio novo: CREDIT_BUREAU_TOKEN_REQUIRED (422) — credit_bureau_type diferente de NONE sem token no payload nem já salvo (RN2). Permissão exigida: mesma de hoje, require_system_role("ADMIN_TENANT") (403 para os demais papéis, sem mudança).
{
"id": "uuid",
"...": "campos já existentes, sem mudança",
"bureau_inquiry": {
"status": "SUCCESS",
"score": 148,
"classification": "E",
"default_probability_percent": "98.80",
"financial_pendencies_count": 0,
"financial_pendencies_total": "0.00",
"protests_count": 0,
"bounced_checks_count": 0,
"presumed_income_range": "DE R$ 3.001 ATE R$ 4.000",
"requested_at": "2026-09-03T14:02:00Z",
"queried_at": "2026-09-03T14:02:03Z"
}
}
bureau_inquiry é null quando o tenant não tem o tipo ativado, o locatário principal nunca foi consultado, o inquilino principal é pessoa jurídica (RN5), ou quem pergunta é AGENCY_USER (RN9, correção pós-aprovação — reversão de CA8: antes ela recebia o campo, agora nunca). status pode ser "PENDING" (RN4b) — a submissão agendou a consulta mas o worker em background ainda não respondeu; requested_at vem preenchido, queried_at vem null, e nenhum campo de dado vem preenchido. Quando a consulta termina e falha, status é "FAILED" e um campo failure_reason (texto) substitui os campos de dado — o valor continua vindo no contrato (é usado na auditoria), mas apps/web não o exibe no painel (nome de fornecedor e motivo técnico ficam só em AuditLog, ação BUREAU_INQUIRY). Permissão exigida é a mesma já existente, credit_analyses:read, sem mudança de código de erro (404 de tenant/imobiliária alheios já existe hoje) — a diferença por papel está só em bureau_inquiry vir ou não preenchido, nunca no status da resposta.
{
"id": "uuid",
"...": "campos já existentes, sem mudança",
"credit_bureau": {
"status": "SUCCESS",
"score": 148,
"...": "mesmo formato de bureau_inquiry acima"
}
}
credit_bureau segue exatamente as mesmas regras de bureau_inquiry (null/PENDING/ FAILED/SUCCESS, AGENCY_USER nunca recebe preenchido) — é o mesmo dado, agora também exposto pelo locatário diretamente, não só através da análise.
Sem corpo. Resposta: o locatário (mesmo formato de GET /renters/{id}), com credit_bureau.status = "PENDING" — a chamada de verdade roda em background, como qualquer agendamento (RN4b); quem quiser o resultado faz polling do mesmo GET.
Erros de negócio novos: CREDIT_BUREAU_NOT_ACTIVE (422) — tenant sem a consulta automática ativada; CREDIT_BUREAU_RENTER_NOT_ELIGIBLE (422) — locatário pessoa jurídica ou sem CPF cadastrado. Permissão exigida: renters:reanalyze_credit_bureau — 403 para quem não tem, inclusive AGENCY_USER (nunca concedida a esse papel no seed de RBAC). Locatário de outro tenant/imobiliária: 404, mesmo filtro de GET/PATCH /renters/{id}.
Migrations: add_credit_bureau_settings_to_tenant_settings (adiciona as duas colunas em tenant_settings, downgrade remove ambas), create_credit_bureau_inquiries (criou a tabela credit_bureau_inquiries; derrubada depois, ver abaixo), credit_bureau_token_text (achado da revisão final — credit_bureau_token era curto demais para o texto cifrado, ver seção 18), credit_bureau_inquiry_background (RN4b — acrescentou PENDING ao enum de status e a coluna requested_at, tornou queried_at opcional) e bureau_no_renter (RN9, correção pós-aprovação — as quatro colunas credit_bureau_* em renters, reaproveitando o enum já criado; derruba credit_bureau_inquiries, que a essa altura não tinha mais escritor nenhum).
13. Critérios de aceite¶
- [x] CA1. Dado um tenant com
credit_bureau_type = NONE(RN1), quando uma Análise de Crédito de inquilino PF é submetida, então nenhumCreditBureauInquiryé criado e o painel não aparece na tela. Provado emapps/api,apps/webeapps/e2e. - [x] CA2. Dado um tenant com
credit_bureau_type = BOAVISTA_CADASTRO_POSITIVO(RN1) e token configurado, quando uma Análise de Crédito de inquilino PF é submetida, então a consulta é agendada automaticamente (RN4) e roda em background (RN4b — correção pós-aprovação), umCreditBureauInquirycom os campos estruturados é gravado (RN6) quando o worker termina, e o painel troca sozinho de "consultando" para o resultado, via polling, sem oANALYSTter digitado nem recarregado nada. Provado emapps/api(com a resposta real que embasou a spec) eapps/web(fixture, incluindo o estadoPENDINGe o polling com timers simulados) — não emapps/e2e: sem CPF de sandbox da API Full (seção 15), o caminho de sucesso de ponta a ponta contra o serviço real fica pendente. O caminho de falha de ponta a ponta contra o worker real, esse sim, está emapps/e2e(CA4/CA8/CA9) desde a correção RN4b. - [x] CA3. Dado o mesmo tenant, quando o inquilino principal é pessoa jurídica, então nenhuma consulta roda e o painel não aparece (RN5). Provado em
apps/apieapps/e2e. - [x] CA4. Dado que a consulta ao Boa Vista falha (timeout ou erro simulado), quando a análise é submetida, então a transição para
SUBMITTEDacontece normalmente, o painel mostra "consulta falhou" com o motivo, e oANALYSTconsegue decidir normalmente (RN7). Provado emapps/api(seis cenários de falha) eapps/e2e(falha real contra a API Full, com token inválido de propósito). - [x] CA5. Dado um
ADMIN_TENANTtentando salvarcredit_bureau_type = BOAVISTA_CADASTRO_POSITIVOsem informar token (e nenhum já salvo), quando ele submete o formulário de Configurações, então recebeCREDIT_BUREAU_TOKEN_REQUIRED(422) e a configuração não é salva pela metade (RN2). Provado emapps/apieapps/web. - [x] CA6. Dado um token já salvo, quando qualquer papel lê
GET /tenant-settings, então a resposta trazcredit_bureau_token_configured: truee nunca o valor do token (RN3) — negativo paraADMIN_GLOBAL,ADMIN_TENANTe qualquer outro papel com acesso de leitura. Provado emapps/apieapps/web. - [x] CA7. Dado um
ANALYST,OPERATORouVIEWERtentandoPATCH /tenant-settingscomcredit_bureau_type, quando a requisição é enviada, então recebe 403 (10.2). Coberto pelo teste de permissão já existente (test_tenant_settings_leitura.py), inalterado por esta spec — o gate (require_system_role("ADMIN_TENANT")) não mudou. - [x] CA8. Invertido na correção pós-aprovação (RN9). Dado um
AGENCY_USERda imobiliária dona da análise, quando ele abre a tela, então não vê o painel do bureau (bureau_inquiry: null, mesmo com dadoSUCCESSjá gravado no locatário) — nem o painel de parecer interno, que já não via (10.1) — e umAGENCY_USERde outra imobiliária recebe 404 ao tentar abrir a mesma análise (10.4). Provado emapps/api,apps/webeapps/e2e; o negativo (outra imobiliária) é o 404 já existente, herdado, não específico desta spec. A versão original deste critério (imobiliária via o painel) foi a decisão revisada e aprovada da spec — a inversão é achado do usuário testando ao vivo, depois de já estar no ar (ver Registro). - [x] CA9. No mobile (375px), o painel aparece sempre aberto (não colapsa), com a mesma disposição rótulo/valor do desktop e sem nenhum campo escondido (7.2), nas duas telas que o exibem (Análise de Crédito e Inquilino). Provado em
apps/webeapps/e2e(sem rolagem horizontal em 375px). - [x] CA10. O resultado do Boa Vista não altera
credit_recommendation— a mesma análise, com e sem o painel, chega à mesma recomendação automática pela regra 3× (RN8). Verificado por leitura estrutural (nenhum código desta spec tocacompute_credit_summary), não por um teste dedicado — achadoMinoraceito na revisão da branch (seção 17). - [x] CA11 (RN9, correção pós-aprovação). Dado um locatário com resultado
SUCCESSde uma análise anterior, quando uma segunda análise dele é submetida, então a consulta não roda de novo — o painel mostra o dado já existente, sem novo job agendado (a trava). Provado emapps/api(test_segunda_analise_do_mesmo_inquilino_ nao_consulta_de_novo); sem cobertura e2e pelo mesmo motivo de CA2 (falta CPF de sandbox para produzir umSUCCESSreal). - [x] CA12 (RN9, correção pós-aprovação). Dado um locatário com resultado
FAILED, quando uma nova análise dele é submetida, então a consulta roda de novo automaticamente (sóSUCCESStrava). Provado emapps/api(test_falha_anterior_nao_trava). - [x] CA13 (RN9, correção pós-aprovação). Dado um
ADMIN_TENANT/ANALYST/OPERATORna tela do locatário ou da análise, quando clica em "Reanalisar", então uma nova consulta é agendada mesmo com umSUCCESSjá existente (a trava é ignorada de propósito), e o botão não aparece paraVIEWERnemAGENCY_USER(403 se a rota for chamada mesmo assim). Provado emapps/api(permissão e trava ignorada) eapps/web(visibilidade do botão nas duas telas).
14. Plano de implementação¶
Ordem obrigatória no monorepo: api → web → e2e.
- [x]
apps/api—app/core/crypto.py(cifra/decifra), variávelTENANT_SECRETS_KEY - [x]
apps/api— modelTenantSettings(+2 campos, enumCreditBureauType) → schema → repository → service (validação RN2) → router - [x]
apps/api— modelCreditBureauInquiry(novo) → schema → repository - [x]
apps/api— servicecredit_bureau_service.py(client HTTP, nunca lança) - [x]
apps/api—credit_analysis_service.submit()chamando o client novo e persistindo o resultado - [x]
apps/api— migrations Alembic (as duas) — aplicadas com sucesso contra o Postgres real do dev-remote no rebuild;downgradeescrito mas não exercitado nele (não há motivo de negócio para reverter agora) - [x]
apps/api— docstrings + Swagger (summary,description,response_description) - [x]
apps/api— testes emtests/(unitários do client, dosubmit(), da validação de tenant-settings, e os negativos de autorização) - [x]
apps/web—BureauInquiryPanel.tsx, inserido emshow.tsx, comdata-testidem todo elemento, responsivo (desktop e mobile) - [x]
apps/web— campo de tipo + token na tela de Configurações, write-only - [x]
apps/e2e— spec cobrindo o que dá para provar sem CPF de sandbox (CA1, CA4, CA8, CA9); CA2 (caminho de sucesso) fica pendente da decisão em aberto da seção 15 - [x]
make test— pytest 840 passed/1 failed (o intermitente conhecido, área não tocada);make test-web202 passed/0 failed.make lintnão passado como comando único;ruff/tscconferidos limpos nos arquivos tocados por esta branch - [x] Validado no browser em desktop e mobile — via
apps/e2e, contra o dev-remote reconstruído (spec novo + 26 specs existentes da mesma tela, sem regressão)
15. Decisões em aberto¶
- [ ] Os CPFs de teste do ambiente de sandbox da API Full (mencionados como existentes, mas sem lista concreta) precisam ser levantados junto ao suporte da API Full antes da fase
apps/e2e— sem eles, o spec e2e não tem como exercitar CA2/CA4 sem gastar saldo real. Risco aceito conscientemente para as fasesapps/api/apps/web, decidido pelo usuário ao aprovar esta spec em 2026-09-03: as fasesapiewebseguem sem bloqueio; só a faseapps/e2efica represada até quem tem a conta na API Full levantar a lista (fora do escopo desta spec resolver sozinha).
16. Revisão do detalhamento¶
Preenchido pelo laço do /spec new, rubrica .claude/rubrics/detalhamento.md. Registrar todas as rodadas, inclusive as reprovadas.
| Rodada | Critical | Important | Minor | Nota | Veredicto |
|---|---|---|---|---|---|
| 1 | 0 | 3 | 0 | 6 | Reprovada |
| 2 | 0 | 2 | 1 | 7 | Reprovada |
| 3 | 0 | 0 | 2 | 9 | Aprovada |
Resultado: aprovada na rodada 3.
Riscos aceitos: nenhum risco de acesso — os dois Minor da rodada 3 (citação incompleta do padrão de Enum de TenantSettings, e a pendência de sandbox sem "aceite" explícito registrado) foram corrigidos no texto antes de fechar o portão.
17. Revisão da implementação¶
Preenchido pelo /spec implement, rubrica .claude/rubrics/implementacao.md. As revisões de fase (api, web, e2e) vão nas primeiras linhas; a da branch inteira, na última.
| Rodada | Escopo | Critical | Important | Minor | Nota | Veredicto |
|---|---|---|---|---|---|---|
| 1 | apps/api | 0 | 1 | 2 | 7 | Reprovada |
| 2 | apps/api | 0 | 0 | 1 | 9 | Aprovada |
| 3 | apps/web | 0 | 0 | 3 | 9 | Aprovada |
| 4 | branch | 0 | 1 | 1 | 8 | Aprovada |
Resultado: aprovada na rodada 4 (branch inteira).
Defeitos aceitos: nenhum — o Important (coluna credit_bureau_token curta demais para o texto cifrado) foi corrigido depois da aprovação, não deixado em aberto (seção 18, Registro, tem o detalhe e a prova contra Postgres real). O Minor (CA10 sem teste dedicado comparando com/sem o painel) permanece — a separação é estrutural (nenhum código desta spec toca compute_credit_summary), e o risco de acoplamento futuro fica registrado aqui em vez de um teste que hoje só provaria o óbvio.
18. Registro¶
- RN4/10.3 corrigido na revisão da fase
apps/api(rodada 1)._run_credit_bureau_inquiryresolviaTenantSettings/Rentera partir decurrent_user.tenant_id, não decredit_analysis.tenant_id— quebrava silenciosamente paraADMIN_GLOBAL(tenant_id is None). Corrigido: o método passou a receber o objetoCreditAnalysisinteiro. Coberto por um teste de regressão novo (test_admin_global_submete_e_a_consulta_roda_com_o_tenant_da_analise), com red-green manual conferido (bug reintroduzido → teste cai → restaurado → volta a passar). apps/e2e/fixtures/limpeza.tsprecisou de um ajuste que a spec não previu. A tabelacredit_bureau_inquiriestem FKON DELETE RESTRICTparacredit_analyses, e a lista de limpeza do e2e (TABELAS) não a conhecia — a primeira execução do spec novo quebrou a faxina inteira (violates foreign key constraint), deixando lixo do próprio teste e arriscando lixo de qualquer outro spec rodando na mesma janela. Adicionada à lista, na posição certa (antes decredit_analyses, mesmo motivo deguarantees/documents). Verificado: a limpeza rodou limpa nas duas execuções seguintes.- CA2 (caminho de sucesso da consulta) não tem prova e2e. Como a seção 15 já registrava como risco aceito, os CPFs de sandbox da API Full nunca foram levantados. O spec e2e novo prova RN1 (tipo
NONE) e RN7/CA4/CA8/CA9 usando um token inválido de propósito — a chamada real à API Full falha de verdade, e é esse caminho de falha que a spec promete não bloquear. O retorno bem-sucedido (score, pendências, protestos reais) está coberto só até a fronteira doapps/api(test_credit_bureau_service.py, com a resposta real que o usuário colou na conversa) e doapps/web(show.test.tsx, com fixture). Falta a prova de que a chamada real completa — request assinado, resposta da API Full, parsing, gravação — funciona de ponta a ponta. Fica para quando os CPFs de sandbox existirem. - Régua do
/specmudou no meio desta spec, a pedido do usuário. De "até 3 rodadas, Important em aberto sempre reprova" para "até 10 rodadas, com nota 0-10: semCritical, nota ≥ 8 aprova mesmo comImportantem aberto". Alterado emCLAUDE.mde noSKILL.mddo/specantes da Parte II ser revisada — as quatro rodadas da Parte II e as três do detalhamento já rodaram sob a régua nova. - Endpoint corrigido depois do commit inicial, com uma chamada real do usuário. A RN4 e o
credit_bureau_service.pyapontavam paracp-cadastrais-score-dividas— um endpoint que não existe para a conta real usada (uma chamada de teste devolveu 500 vazio). O usuário compartilhou a requisição salva na coleção Postman da API Full ("Dívidas e Crédito › Boa Vista Essencial Positivo") e, depois, o retorno de uma consulta real com o próprio CPF: o endpoint certo éPOST /api/scpc-boavista, com um segundo campo obrigatório no corpo,"link": "scpc-boavista"— que apesar do nome não é webhook nem URL, é um identificador fixo do produto. O formato do retorno (dados. HEADER/CREDCADASTRAL/…) é idêntico ao exemplo usado para desenhar_extrair(), que não precisou mudar. O token e o CPF reais usados na consulta não foram gravados em nenhum arquivo do repositório — só o formato da resposta, já sem dado pessoal, embasou o teste novo (test_chama_o_endpoint_e_o_link_certos). - Achado
Importantda revisão da branch inteira, corrigido depois da aprovação (nota 8).tenant_settings.credit_bureau_tokeneraVARCHAR(500), curto demais para o texto cifrado — um token real de ~300-400 caracteres já estoura o limite depois do Fernet. Corrigido paraText(model + migration novam5o7q9s1u3w4). A prova não é o teste unitário — SQLite não impõe limite deVARCHAR(N), então o teste passa nas duas versões da coluna e não teria pegado o defeito sozinho, achado ao tentar o red-green. A prova real foi contra o Postgres do dev-remote:PATCH /settingscom um token de 400 caracteres (cifra para ~630) devolveu 200, depois de aplicar a migration e reiniciar o container — antes dela, o mesmo request devolveria 500. - Achado do usuário testando ao vivo no celular, depois do commit acima. "O nome Boa Vista não deve ficar visível é interno da garantidora, e o erro 400 precisa ter algo mais amigável, mas em auditoria deve apresentar a chamada com o retorno." Três mudanças:
BureauInquiryPanel.tsxdeixou de exibir o nome do fornecedor ("Consulta ao Boa Vista" → "Consulta de crédito", nos dois estados) e, no estadoFAILED, trocou ofailure_reasonbruto (às vezes um código HTTP) por uma frase fixa, sem detalhe técnico. Ofailure_reasoncontinua vindo no contrato de API (CreditBureauInquiryResponse) — só o painel deixou de mostrá-lo.credit_analysis_service.pypassou a gravar umAuditLog(action="BUREAU_INQUIRY") dentro de_run_credit_bureau_inquiry, com o nome do fornecedor, status, score/motivo de falha — o detalhe que saiu do painel. A gravação é best-effort, numtry/exceptque só loga: uma falha aqui não pode reabrir o problema que a RN7 já fecha (a consulta nunca bloqueia a submissão).- A auditoria estava sendo escrita, mas nunca gravava —
CreditBureauInquiryStatusera usado em_run_credit_bureau_inquirysem import (sóCreditBureauInquiry, o model, estava importado), e oexcept Exceptiondo próprio item 2 engolia oNameErrorsilenciosamente a cada submissão. Achado escrevendo o teste de regressão (test_consulta_gera_registro_de_auditoria_com_o_fornecedor): rodou vermelho antes da correção do import, verde depois — red-green manual também conferido revertendo o import de novo. Os testes antigos deste arquivo não pegavam porque nenhum lia a tabelaAuditLog.
Textos desatualizados corrigidos junto: a asserção de show.test.tsx que esperava "não foi possível consultar o Boa Vista" (não existe mais) e as duas do spec e2e (analise-credito-boavista.spec.ts, CA4/CA9) com a mesma frase — agora conferem a mensagem genérica nova e a ausência do nome do fornecedor no painel. - Ordem do painel ajustada a pedido do usuário. BureauInquiryPanel subiu para antes do PolicyPanel em show.tsx — o dado externo do bureau embasa a leitura da política, não o contrário. Puramente visual (sem data-testid novo, sem mudança de contrato); credit-analysis-detail-order.spec.ts (spec 0025), que afere ordem por posição vertical real, não cita o painel do bureau porque a análise ali usada tem credit_bureau_type: NONE — segue passando (10/10) sem precisar de alteração. Confirmado também que analise-credito-boavista.spec.ts continua verde com o painel na posição nova. - RN4b — a consulta virou background, achado do usuário testando ao vivo pela terceira vez. "A consulta da Boa Vista demorou para responder, gastou o crédito e não recebi o retorno, e ainda travou a tela, precisamos criar uma forma de fazer isso em background e garantir o retorno." O desenho síncrono de RN4 (chamada HTTP dentro da transação de submit()) tinha dois defeitos reais: a tela ficava presa pelo tempo da chamada (até 10s de leitura), e uma conexão que caísse no meio (rede de celular, o cenário relatado) deixava a chamada paga em curso no servidor sem garantia nenhuma de que o resultado chegasse a quem esperava.
Duas decisões só do usuário, perguntadas antes de desenhar (a spec não inventa infraestrutura nem política de retry): mecanismo de background — fila dedicada (Redis) em vez de BackgroundTasks do FastAPI, decisão explícita por resistir a um restart da API no meio de uma consulta em curso e não competir por CPU com quem serve requisição; e política de nova tentativa — nenhuma automática, para não gastar o crédito duas vezes numa falha que pode ser real (CPF inválido), não só transitória. Dentro de "fila dedicada", a escolha de arq (não Celery/RQ) foi técnica, não perguntada: é nativamente assíncrono, e o resto do backend já é async de ponta a ponta (asyncpg, SQLAlchemy async, httpx async) — Celery/RQ exigiriam rodar código async dentro de worker síncrono.
O que mudou, por app: - apps/api — _run_credit_bureau_inquiry virou _schedule_credit_bureau_inquiry (grava PENDING + agenda o job, não chama mais a Boa Vista); a chamada de verdade foi para app/services/credit_bureau_worker_service.py (novo), rodando dentro de app/worker.py (novo, arq). Infra nova: app/core/queue.py (pool arq lazy), REDIS_URL em app/core/config.py, migration nova (PENDING no enum, requested_at, queried_at nullable), serviços redis/worker nos três docker-compose.yml, alvo dev-worker/worker-dev nos dois Makefile. - apps/web — BureauInquiryPanel ganhou o estado PENDING ("consultando", com CircularProgress); show.tsx ganhou um useEffect de polling (3s, teto de 60s) que busca só a análise enquanto bureau_inquiry.status === "PENDING". - apps/e2e — o spec existente passou a esperar o worker de verdade (container worker do dev-remote, mesmo Redis da API) em vez de medir um resultado que o submit() já teria embutido. A prova de que submit() não bloqueia mais é cronometrada, não a observação de um PENDING transitório — na prática, o worker respondeu rápido o bastante (chamada com token inválido falha em menos de 1s) para o registro já estar FAILED quando a tela termina de montar, o que tornaria uma asserção "deve mostrar PENDING" inerentemente instável.
Um bug real encontrado escrevendo o teste do worker, não pela revisão. A auditoria usava CreditBureauInquiryStatus sem importar o nome no novo módulo — NameError silencioso, engolido pelo próprio try/except que protege a gravação da auditoria (a mesma classe de defeito da correção anterior desta seção, em módulo diferente). Achado pelo teste test_worker_fecha_pending_como_success cobrindo a linha de auditoria; red (import faltando) → green (import corrigido), revertido e confirmado de novo.
Verificação: pytest de test_credit_analysis_bureau_inquiry.py reescrito para a arquitetura em duas fases (12 casos, incluindo agendamento com Redis indisponível, worker processando job já concluído — idempotência —, e falha inesperada dentro do worker nunca deixando PENDING) — 12/12; suíte inteira da API, 849/850 (1 vermelho é o intermitente já catalogado em CLAUDE.md, não relacionado). apps/web: 13/13 em show.test.tsx (2 casos novos, um com vi.useFakeTimers provando o polling end-to-end), suíte inteira 204/204. apps/e2e: analise-credito-boavista.spec.ts 3/3 contra o worker real do dev-remote reconstruído (redis, worker, api, web), e credit-analysis-detail-order.spec.ts (spec 0025) 8/8 sem alteração — confirma que o polling novo não interfere na tela para tenants sem a consulta ativada. Migration aplicada e revertida (downgrade/upgrade) contra o Postgres real do dev-remote antes de confirmar o schema. - A resposta da API Full não traz custo, consumo nem saldo de créditos — conferido contra uma resposta real completa que o usuário compartilhou. A dúvida era se dados.HEADER carregava algum indicador financeiro da consulta. Não carrega: HEADER. DADOS_RETORNADOS é só um mapa de flags ("0"/"1") dizendo quais blocos de dado vieram na resposta (ex.: SCORE: "1", ENDERECOS: "0"), e HEADER.CONTROLE traz QUANTIDADE_OCORRENCIAS e a fonte/status de cada ocorrência — nenhum dos dois é medida de custo ou saldo. Nada em CREDCADASTRAL também. Continua não havendo dado de saldo/consumo em lugar nenhum do payload, o que reafirma o limite já registrado na seção 5 ("Conta compartilhada da plataforma... esta spec não cobre uma conta única da Garantora"): se a garantidora quiser ver quanto crédito resta na própria conta da API Full, a fonte teria que ser o painel da própria API Full (fora desta plataforma), ou um novo endpoint da API Full que esta consulta não usa — não há o que extrair do que já chega hoje. Nenhuma mudança de código: raw_response já grava o dados inteiro (HEADER incluído) por análise, então o dado — se um dia a API Full passar a devolvê-lo — já ficaria capturado sem precisar de alteração no parser. O exemplo completo da resposta está no Apêndice A — confirmado pelo usuário como dado fictício, de exemplo, não uma pessoa real, então guardá-lo aqui não é o mesmo risco do exemplo usado para achar o endpoint certo (seção 18, acima), que usava o CPF real do próprio usuário. - RN9 — o bureau virou característica do locatário, não da análise, com trava e reanálise manual. Terceira correção pós-aprovação, pedido de uma vez só: "Quero que todas essas informações estejam associadas ao inquilino pode colocar em um campo no banco jsonb. Na análise de crédito e no inquilino adicionar um painel com as informações capturadas. Somente a garantidora tem acesso a informação a imobiliária não tem. Coloque também uma trava que somente chamar a api para verificar o crédito se não tiver feito antes e um botão de reanalisar mas somente para garantidora."
Três decisões perguntadas antes de desenhar (não inventadas): manter CreditBureauInquiry como histórico por análise além do cache no locatário, ou só o locatário — resposta: só o locatário, mais simples, sem duas fontes da mesma verdade divergindo; um FAILED anterior também trava a próxima análise, ou só SUCCESS trava — resposta: só SUCCESS trava, para uma falha transitória não deixar o inquilino sem dado até alguém lembrar de reanalisar; o botão "Reanalisar" fica só no inquilino, ou nos dois painéis — resposta: nos dois, mesma ação, dois lugares onde o dado aparece.
O que mudou, por app: - apps/api — Renter ganhou as quatro colunas credit_bureau_*; CreditBureauInquiry (tabela por análise) foi derrubada; a decisão "agenda ou não" (elegibilidade + trava) virou um módulo próprio, app/services/credit_bureau_schedule.py, compartilhado entre o agendamento automático (CreditAnalysisService._schedule_credit_bureau_inquiry, force=False) e o manual (RenterService.reanalyze_credit_bureau, novo, force=True); o worker (complete_credit_bureau_inquiry) passou a fechar o registro do locatário; CreditAnalysisResponse.bureau_inquiry virou derivado (lido do locatário principal na hora de montar a resposta, não mais uma FK); rota nova, POST /renters/{id}/reanalyze-credit-bureau; permissão nova, renters:reanalyze_credit_bureau. - apps/web — BureauInquiryPanel saiu de pages/credit-analyses/components/ para components/shared/ (agora usado em duas telas), ganhou testIdPrefix (preserva os data-testid antigos na Análise de Crédito via credit-analysis-bureau, novos via renter-bureau no Inquilino) e o botão "Reanalisar" (onReanalyze/reanalyzing); pages/renters/show.tsx ganhou o painel + o mesmo mecanismo de polling que credit-analyses/show.tsx já tinha (RN4b), duplicado porque são dois recursos (GET /renters/{id} vs GET /credit-analyses/{id}) e o polling busca o próprio. - apps/e2e — o teste que provava CA8 (imobiliária vê o painel) foi invertido para provar o oposto — não é um caso novo, é o mesmo caso, com a asserção trocada.
Uma decisão técnica não perguntada, e por quê: o usuário disse "jsonb"; o campo virou JSON genérico do SQLAlchemy, não JSONB do Postgres — mesma convenção já documentada no CLAUDE.md da API ("SQLite compatibility: avoid PostgreSQL-specific column types"), que existe para o pytest rodar sem Docker. raw_response (dentro do JSON) já usava JSON genérico desde a aprovação original da spec; manter a mesma escolha para o campo novo evita duas convenções concorrentes na mesma tabela.
Um achado do meta-teste, não do usuário, mudou o desenho de permissão no meio do caminho. A primeira versão desta correção criava renters:read_credit_bureau como permissão RBAC — pareceu simétrico com renters:reanalyze_credit_bureau, e as duas entraram no seed. test_authorization_coverage.py::test_toda_permissao_definida_e_ exigida (meta-teste de permissão órfã, mencionado no CLAUDE.md da raiz, seção make preflight) reprovou: a permissão era concedida a vários papéis, mas nunca exigida de um jeito que a varredura reconhece (require_permission no router, ou exigir_permissao no serviço) — o código real usava current_user.has_permission(...) direto, para redigir um campo condicionalmente, não para bloquear a requisição inteira. Em vez de inventar um terceiro padrão só para este caso, ou enfraquecer a varredura do meta-teste, a permissão foi removida: ver o resultado do bureau passou a ser identidade (current_user.partner_agency_id is not None), o mesmo padrão que within_agency/agency_scope já usam para todo o resto do isolamento por imobiliária desta spec (seção 10.4) — mais consistente com o restante do código do que uma permissão nova teria sido. renters:reanalyze_credit_bureau continuou permissão RBAC normal, porque essa sim é uma ação que o router bloqueia via require_permission, satisfazendo o meta-teste sem ajuste nenhum.
Verificação: pytest novo, test_renter_credit_bureau.py (17 casos — trava, elegibilidade, worker no locatário, idempotência de reentrega, reanalyze_credit_ bureau com as duas recusas 422, falha ao agendar fecha FAILED, visibilidade por identidade) — 17/17; test_credit_analysis_bureau_inquiry.py reescrito (6 casos, focado no que é específico da análise: agenda no locatário certo, segunda análise do mesmo inquilino não reconsulta — a trava, vista do lado da análise —, ADMIN_GLOBAL, AGENCY_USER nunca vê) — 6/6; test_authorization_coverage.py — verde depois da correção do desenho de permissão; suíte inteira da API — 859/861 (os dois vermelhos são os intermitentes já catalogados em CLAUDE.md, SQLite/desempate de UUID, não relacionados). apps/web: show.test.tsx de renters/ (novo, 7 casos — painel ausente/PENDING/SUCCESS/FAILED, botão Reanalisar visível/escondido por permissão, clique aciona a API, polling), show.test.tsx de credit-analyses/ inalterado e ainda verde (13/13, os data-testid antigos sobreviveram à mudança de componente); suíte inteira — 211/211. Migration aplicada e revertida (downgrade/upgrade) contra o Postgres real do dev-remote antes de confirmar o schema. tsc --noEmit limpo em todo o app depois de mover o componente e trocar os dois pontos que o importavam. - RN9b (modo sandbox) — um bug de colisão de nome, achado pelo próprio teste que ele quebrou. credit_bureau_worker_service.py importava from app.core.config import settings no topo do módulo, e a função reatribuía settings = await settings_repo. get_by_tenant(...) — uma linha que já existia antes desta correção, agora escondendo o settings do módulo dentro do próprio escopo da função. O acesso a settings. CREDIT_BUREAU_SANDBOX_MODE (a TenantSettings do tenant, não a configuração da aplicação) estourava AttributeError, engolido pelo except Exception da própria gravação de auditoria (best-effort, de propósito — RN7) — a consulta terminava SUCCESS normalmente, só a linha de auditoria nunca era gravada. test_worker_fecha_ pending_do_locatario_como_success, já existente, pegou sozinho (assert len(linhas) == 1 falhando com 0 == 1). Corrigido importando com alias (from app.core.config import settings as app_settings) nos dois pontos que o usavam. Reforçado o teste com uma asserção nova no campo payload["sandbox"], e feito o red-green manual: revertido o alias → o teste cai com o mesmo AttributeError no log → restaurado → volta a passar.
Verificação final: suíte inteira da API rodada duas vezes depois da correção — a primeira, contaminada por um docker cp no meio da execução (mesma armadilha já catalogada no CLAUDE.md: recriar/alterar o container enquanto o pytest roda mede estado inconsistente), foi descartada; a segunda, limpa, fechou em 864 passed, 0 failed em 377s — nem o intermitente conhecido (test_partner_agency_status_filter.py) flakeou nesta rodada. test_credit_bureau_service.py — 10/10, incluindo os dois casos novos do modo sandbox, cada um com red-green manual próprio (guarda de produção desativada → create_app() não levanta RuntimeError → restaurada → volta a levantar; e o teste do próprio modo sandbox nunca precisou de reversão, porque nasceu depois do código que prova). Deploy consolidado via rebuild de imagem (api e worker, mesma imagem, comando trocado) — não ficou como estado só de docker cp. - RN9c, e um segundo efeito colateral do RN9b — a suíte deixou de ser hermética. Depois de infra/dev-remote/.env ganhar CREDIT_BUREAU_SANDBOX_MODE=true (RN9b), todo pytest rodado dentro do container do dev-remote passou a herdar esse true do ambiente real — porque é lá que a suíte roda hoje, sem uv no host (CLAUDE.md, "Estado do ferramental"). Ao escrever test_ativar_sem_token_e_rejeitado (RN2) com a relaxação nova, o teste passou a falhar: o container já tinha o modo ligado antes de o teste rodar, e a validação que ele verifica não se aplicava mais. Corrigido em tests/conftest.py, fixando settings.CREDIT_BUREAU_SANDBOX_MODE = False no import — mesmo padrão já existente para settings.BCRYPT_ROUNDS, mesma razão: a suíte precisa medir o comportamento descrito, não o que o ambiente que a hospeda calha de ter ligado. Quem quiser sandbox ligado dentro de um teste usa monkeypatch.setattr (como já faziam os dois testes do RN9b, e como os três novos do RN9c fazem).
A régua de "o token só é exigido fora do sandbox" ficou provada nos dois lados: test_ativar_sem_token_e_rejeitado (sandbox off, herdado do conftest) continua recusando; test_sandbox_ativo_dispensa_o_token (sandbox ligado por monkeypatch, só naquele teste) aceita a mesma requisição. Red-green manual na condição nova (and not app_settings.CREDIT_BUREAU_SANDBOX_MODE): removida → test_sandbox_ativo_ dispensa_o_token cai com 422 CREDIT_BUREAU_TOKEN_REQUIRED → restaurada → volta a passar. Mesmo red-green do lado do apps/web, no bloco condicional que esconde o campo de token: {form.credit_bureau_sandbox_active ? <Alert/> : <Box/>} trocado por {false ? <Alert/> : <Box/>} → os dois testes que esperam o banner caem (getByTestId não encontra) → restaurado → volta a passar.
Verificação: test_tenant_settings_credit_bureau.py — 8/8 (5 já existentes + 3 novos: sandbox inativo por padrão, sandbox aparece no GET, sandbox dispensa o token). Suíte inteira da API depois da correção do conftest — 866 passed, 1 failed (o intermitente já catalogado, test_partner_agency_search.py::test_busca_alcanca_ quem_esta_fora_da_primeira_pagina — SQLite/desempate de UUID, não relacionado). apps/web: pages/settings/index.test.tsx — 8/8 (5 já existentes + 3 novos); suíte inteira do apps/web — 214/214 (211 da medição anterior + os 3 novos).
Apêndice A — exemplo completo de retorno da API Full¶
Resposta de POST /api/scpc-boavista para o CPF 12345678909, compartilhada pelo usuário e confirmada como dado fictício (não uma pessoa real) — por isso pode ficar aqui, diferente do exemplo usado para achar o endpoint certo (seção 18), que usava o CPF real do próprio usuário e não foi persistido em nenhum arquivo.
Referenciado por RN4 (formato de dados usado para desenhar _extrair()), RN6 (campos gravados em CreditBureauInquiry/raw_response) e pelo achado do Registro sobre custo/saldo não estarem no payload.
{
"status": "sucesso",
"API Full": "https://doc.apifull.com.br",
"dados": {
"HEADER": {
"PARAMETROS": {
"CPFCNPJ": "12345678909"
},
"DADOS_RETORNADOS": {
"DADOS_RECEITA_FEDERAL": "0",
"ALERTA_DOCUMENTOS_ROUBADOS": "0",
"DADOS_AGENCIA_BANCARIA": "0",
"PROTESTO_SINTETICO": "0",
"PROTESTO_ANALITICO": "1",
"ENDERECOS": "0",
"FALENCIAS_ACOES_RECUPERACOES": "0",
"INFORMACOES_ELEITORAIS": "0",
"QUADRO_SOCIETARIO": "0",
"PARTICIPACAO_EM_EMPRESAS": "0",
"PENDENCIAS_INTERNAS": "0",
"PENDENCIAS_FINANCEIRAS": "1",
"INFORMACOES_DA_EMPRESA": "0",
"CCF_BACEN": "1",
"CCF_VAREJO": "1",
"RECHEQUE_ONLINE": "0",
"RECHEQUE": "0",
"CONTUMACIA": "1",
"ENDERECO_DO_CEP": "0",
"PASSAGENS_COMERCIAIS": "1",
"INFORMACOES_ALERTAS_RESTRICOES": "1",
"SCORE": "1",
"ADMINISTRADORES_DA_EMPRESA": "0",
"TITULOS_A_VENCER": "0",
"HISTORICO_DE_PAGAMENTOS": "0",
"REFERENCIA_DE_NEGOCIOS": "0",
"PRINCIPAIS_FORNECEDORES": "0",
"IDENTIFICACAO_PESSOA_FISICA": "1",
"OCUPACAO_PESSOA_FISICA": "0",
"PARTICIPACAO_EM_FALENCIAS": "0",
"TELEFONE_FIXO": "0",
"TITULAR_DO_TELEFONE": "0",
"OUTROS_TELEFONES": "0",
"TELEFONE_CELULAR": "0",
"PARENTES": "0",
"CONTATOS": "0",
"LOCAIS_TRABALHO": "0",
"TELEFONE_COMERCIAL": "0",
"DADOS_GERAIS": "0",
"VIZINHOS": "0",
"RESIDENTES": "0",
"BENEFICIO": "0",
"EMAILS": "0",
"INFOBUSCA": "0",
"VINCULO_MESMO_ENDERECO": "0",
"SOMENTE_TELEFONE": "1",
"SOMENTE_ENDERECO": "0",
"RELATORIO_TEXTO": "0",
"VEICULOS_POR_DOCUMENTO": "0",
"CNH": "0",
"AGREGADOS": "0",
"BIN_NACIONAL": "0",
"BIN_ESTADUAL": "0",
"GRAVAME": "0",
"LEILAO": "0",
"CONFERENCIA_MOTOR_E_CHASSI": "0",
"HISTORICO_PROPRIETARIOS": "0",
"HISTORICO_ROUBO_FURTO": "0",
"ROUBO_FURTO": "0",
"PERDA_TOTAL": "0",
"ALERTAS": "0",
"RECALL": "0",
"DPVAT": "0",
"DECODIFICADOR_DE_CHASSI": "0",
"PRECIFICADOR": "0",
"RENAJUD": "0",
"RENACH": "0",
"CRLV": "0",
"REMARKETING": "0",
"INDICIO_SINISTRO": "0",
"PARECER_TECNICO": "0",
"PENDENCIAS_REFIN": "0",
"PENDENCIAS_VENCIDAS": "0",
"LOCALIZADOR_AGREGADOS": "0",
"CARACTERISTICAS_ADICIONAIS": "0",
"LAUDO_CAUTELAR": "0",
"DECISAO": "0",
"LIMITE_CREDITO": "0",
"FATURAMENTO_PRESUMIDO": "0",
"PARTICIPACAO_SOCIO_ADM_OUTRAS_EMPRESAS": "0",
"PARTICIPACOES_EMPRESAS_CONSULTADAS": "0",
"HIST_CONSULTAS": "0",
"ACOES_CIVEIS": "0",
"FALENCIA_RECUPERACAO_JUDICIAL": "0",
"ANALISE_KILOMETRAGEM": "0",
"PONTUALIDADE_PAGTO": "0",
"PROPRIETARIO_ATUAL_VEICULO": "0",
"VALIDACAO_PESSOA_FISICA": "0",
"VALIDACAO_PF_BASICA": "0",
"VALIDACAO_PF_BIOMETRIA_FACE": "0",
"VALIDACAO_PF_CNH": "0",
"VALIDACAO_PF_DOCUMENTO": "0",
"VALIDACAO_PF_ENDERECO": "0",
"VALIDACAO_PF_FILIACAO": "0",
"AUTO_VISTORIA": "0",
"INDICE_SECURITARIO": "0",
"CALCULO_VALORIZACAO": "0",
"ATPVE": "0",
"CREDITO_CONCEDIDO": "0",
"RENDA_PRESUMIDA": "1",
"COMPROMETIMENTO_PAGTO": "0",
"LEILAO_CONJUGADO": "0",
"CSV": "0",
"RENAINF": "0",
"RELATORIO_ARQUIVO": "0",
"INDICIO_SINISTRO_CONJUGADO": "0",
"CONSULTA_GENERICA": "0",
"BATIDOS": "0",
"RELATORIO_SCR": "0",
"CADIN": "0",
"ACOES_TRABALHISTAS": "0",
"RELATORIO_SCR_SINTETICO": "0",
"CPR": "0",
"FICHA_TECNICA_VEICULAR": "0",
"CHECK_LIST_VEICULAR": "0",
"DIVIDA_ATIVA_PESSOA_FISICA": "0",
"DIVIDA_ATIVA_PESSOA_JURIDICA": "0",
"PENDENCIAS_ISP": "0",
"RELATORIO_JURIDICO_EMPRESARIAL": "0",
"INFORMACOES_CRIMINAIS": "0",
"PROCURADOS_JUSTICA": "0",
"RELATORIO_JURIDICO_PESSOA_FISICA": "0",
"DETALHAMENTO_ACAO_JUDICIAL": "0",
"RELATORIO_IMOVEIS": "0",
"DEBITOS_IPVA": "0",
"DEBITO_DIRETO_AUTORIZADO": "0",
"CONTROLE_POSITIVO": "0",
"ALERTA_INDICIO": "0",
"RELATORIO_CREDITICIO_ARQUIVO": "0",
"IMAGENS_VEICULO": "0",
"RELATORIO_SCR_ENCAPSULADO": "0",
"RESTRICOES_FINANCEIRAS": "0",
"ALERTA_VEICULAR_PROPRIEDADE": "0",
"MOTOR_CREDITO": "0",
"ALTERACAO_CARACTERISTICAS": "0",
"DEBITOS_VEICULARES": "0",
"RESTRICOES_SINTETICAS": "0",
"ACOES_JUDICIAIS_COMPLETAS": "0",
"PERIODO_RELACIONAMENTO_FORNECEDORES": "0",
"EVENTOS_PESSOA_JURIDICA": "0",
"CREDITO_OBTIDO": "0",
"COMPROMISSOS": "0",
"PAGAMENTO_PONTUAL": "0",
"PAGAMENTO_ATRASADO": "0",
"COMPROMETIMENTO_FUTURO": "0",
"ANTECEDENTES_PESSOAIS": "0",
"COMUNICADO_VENDA": "0",
"ANTT_TRANSPORTADOR": "0",
"ANTT_VEICULO": "0",
"HIST_TRANSFERENCIA_NACIONAL": "0"
},
"CONTROLE": {
"QUANTIDADE_OCORRENCIAS": "1",
"OCORRENCIAS": [
{
"CONTEUDO": "3",
"FONTE": "58",
"STATUS": "1"
}
],
"CACHE_GERAL": []
}
},
"CREDCADASTRAL": {
"IDENTIFICACAO_PESSOA_FISICA": {
"STATUS_RETORNO": {
"CODIGO": "1",
"DESCRICAO": "CONSULTA CONCLUIDA COM SUCESSO"
},
"CPF_NUMERO": "12345678909",
"CPF_SITUACAO": "CANCELADO",
"CPF_ORIGEM": "PR, SC",
"CPF_DATA_ATUALIZACAO_RECEITA": "27/02/2021",
"RG_NUMERO": "0000222333444",
"RG_UF": "DF",
"RG_DATA_EMISSAO": "-",
"NOME": "DAIANA LASSOLLI",
"NASCIMENTO": "27/04/2000",
"NACIONALIDADE": "",
"NATURALIDADE": "",
"SEXO": "FEMININO",
"GRAU_INSTRUCAO": "NAO INFORMADO",
"NUMERO_DEPENDENTES": "0",
"ESTADO_CIVIL": "NÃO INFORMADO",
"MAE": "JULIA ROMERO",
"END_LOGRADOURO": " ",
"END_BAIRRO": "",
"END_CIDADE": "",
"END_UF": "",
"END_CEP": "",
"TELEFONE": "",
"TITULO_ELEITOR": "0000000000000",
"NOME_PAI": "",
"NOME_SOCIAL": "",
"EMAIL": "",
"NUMERO_PIS": "",
"INDICACAO_OBITO": "",
"FAIXA_RENDA": "",
"CLASSE_SOCIAL": "",
"PODER_AQUISITIVO": "",
"ULTIMO_IMPOSTO_RENDA": "",
"CARTAO_NACIONAL_SAUDE": "",
"QTD_DEPENDENTES_BOLSA_FAMILIA": ""
},
"INFORMACOES_ALERTAS_RESTRICOES": {
"STATUS_RETORNO": {
"CODIGO": "1",
"DESCRICAO": "CONSULTA CONCLUIDA COM SUCESSO"
},
"QUANTIDADE_OCORRENCIA": "0",
"OCORRENCIAS": []
},
"PEND_FINANCEIRAS": {
"STATUS_RETORNO": {
"CODIGO": "1",
"DESCRICAO": "CONSULTA CONCLUIDA COM SUCESSO"
},
"QUANTIDADE_OCORRENCIA": "0",
"VALOR_TOTAL": "0",
"ULTIMO_VENCIMENTO": "",
"DATA_PRIMEIRO": "",
"TOTAL_CREDORES": "",
"VALOR_MAIOR": "",
"VALOR_PRIMEIRO": "",
"PERIODO_INICIAL": "",
"PERIODO_FINAL": "",
"DATA_MAIOR": "",
"PROVEDORES": [
{
"PROVEDOR": "BASE III"
}
],
"OCORRENCIAS": []
},
"PROTESTOS": {
"STATUS_RETORNO": {
"CODIGO": "1",
"DESCRICAO": "CONSULTA CONCLUIDA COM SUCESSO"
},
"VALOR_TOTAL": "",
"ULTIMO_PROTESTO": "",
"QUANTIDADE_OCORRENCIA": "0",
"DATA_PRIMEIRO": "",
"VALOR_PRIMEIRO": "",
"DATA_ULTIMO": "",
"VALOR_ULTIMO": "",
"DATA_MAIOR": "",
"VALOR_MAIOR": "",
"OCORRENCIAS": []
},
"CH_SEM_FUNDOS_VAREJO": {
"STATUS_RETORNO": {
"CODIGO": "1",
"DESCRICAO": "CONSULTA CONCLUIDA COM SUCESSO"
},
"QUANTIDADE_OCORRENCIA": "0",
"CORRENTISTA": "",
"TIPO_PESSOA": "",
"CPFCNPJ": "",
"OCORRENCIAS": []
},
"CH_SEM_FUNDOS_BACEN": {
"STATUS_RETORNO": {
"CODIGO": "1",
"DESCRICAO": "CONSULTA CONCLUIDA COM SUCESSO"
},
"QUANTIDADE_OCORRENCIA": "0",
"CORRENTISTA": "",
"TIPO_PESSOA": "",
"CPFCNPJ": "",
"OCORRENCIAS": []
},
"CONTUMACIA": {
"STATUS_RETORNO": {
"CODIGO": "1",
"DESCRICAO": "CONSULTA CONCLUIDA COM SUCESSO"
},
"QUANTIDADE_OCORRENCIA": "0",
"DATA_PRIMEIRA_OCORRENCIA": "",
"DATA_ULTIMA_OCORRENCIA": "",
"OCORRENCIAS": []
},
"SCORES": {
"STATUS_RETORNO": {
"CODIGO": "1",
"DESCRICAO": "CONSULTA CONCLUIDA COM SUCESSO"
},
"QUANTIDADE_OCORRENCIAS": "1",
"OCORRENCIAS": [
{
"LAYOUT": "SCSIMPLE2020",
"TIPO_SCORE": "NOVO RISCO 6 MESES",
"SCORE": "148",
"EXECUCAO": "N",
"DESCR_PLANO": "CREDITO",
"DESCR_SCORE": "NOVO RISCO 6 MESES",
"CLASSIF_NRO": "15",
"CLASSIF_ABC": "E",
"PROBABILIDADE_INADIMPLENCIA": "98,80",
"TEXTO": "DE CADA 100 PESSOAS CLASSIFICADAS NESTA CLASSE DE SCORE, E PROVAVEL QUE 99 APRESENTEM DEBITOS NO MERCADO NOS PROXIMOS 6 MESES.",
"RISCO": "NULO",
"MODELO_SCORE": "",
"INFORMANTE": "BASE III"
}
]
},
"SOMENTE_TELEFONE": {
"STATUS_RETORNO": {
"CODIGO": "1",
"DESCRICAO": "CONSULTA CONCLUIDA COM SUCESSO"
},
"REGISTRO_LOCALIZADO": "0"
},
"PASSAGENS_COMERCIAIS": {
"STATUS_RETORNO": {
"CODIGO": "1",
"DESCRICAO": "CONSULTA CONCLUIDA COM SUCESSO"
},
"QUANTIDADE_OCORRENCIA": "35",
"OCORRENCIAS": [
{ "DATA_CONSULTA": "26/02/2026", "HORA_CONSULTA": "---", "CLIENTE_CONSULTA": "ANALISE DE CREDITO II", "TELEFONE_CLIENTE": "---", "CIDADE_UF_CLIENTE": "---" },
{ "DATA_CONSULTA": "24/02/2026", "HORA_CONSULTA": "---", "CLIENTE_CONSULTA": "SCLOUD PAY SOLUCOES DE PAGAMEN LTDA", "TELEFONE_CLIENTE": "---", "CIDADE_UF_CLIENTE": "---" },
{ "DATA_CONSULTA": "23/02/2026", "HORA_CONSULTA": "---", "CLIENTE_CONSULTA": "SCLOUD PAY SOLUCOES DE PAGAMEN LTDA", "TELEFONE_CLIENTE": "---", "CIDADE_UF_CLIENTE": "---" },
{ "DATA_CONSULTA": "20/02/2026", "HORA_CONSULTA": "---", "CLIENTE_CONSULTA": "SCLOUD PAY SOLUCOES DE PAGAMEN LTDA", "TELEFONE_CLIENTE": "---", "CIDADE_UF_CLIENTE": "---" },
{ "DATA_CONSULTA": "19/02/2026", "HORA_CONSULTA": "---", "CLIENTE_CONSULTA": "ALUDE TECNOLOGIA LTDA", "TELEFONE_CLIENTE": "---", "CIDADE_UF_CLIENTE": "---" },
{ "DATA_CONSULTA": "19/02/2026", "HORA_CONSULTA": "---", "CLIENTE_CONSULTA": "SCLOUD PAY SOLUCOES DE PAGAMEN LTDA", "TELEFONE_CLIENTE": "---", "CIDADE_UF_CLIENTE": "---" },
{ "DATA_CONSULTA": "10/02/2026", "HORA_CONSULTA": "---", "CLIENTE_CONSULTA": "MS-NVR/NEWPARCE", "TELEFONE_CLIENTE": "---", "CIDADE_UF_CLIENTE": "---" },
{ "DATA_CONSULTA": "09/02/2026", "HORA_CONSULTA": "---", "CLIENTE_CONSULTA": "MARIELE MOVEIS", "TELEFONE_CLIENTE": "---", "CIDADE_UF_CLIENTE": "---" },
{ "DATA_CONSULTA": "06/02/2026", "HORA_CONSULTA": "---", "CLIENTE_CONSULTA": "ANALISE DE CREDITO II", "TELEFONE_CLIENTE": "---", "CIDADE_UF_CLIENTE": "---" },
{ "DATA_CONSULTA": "28/01/2026", "HORA_CONSULTA": "---", "CLIENTE_CONSULTA": "ALUDE TECNOLOGIA LTDA", "TELEFONE_CLIENTE": "---", "CIDADE_UF_CLIENTE": "---" },
{ "DATA_CONSULTA": "22/01/2026", "HORA_CONSULTA": "---", "CLIENTE_CONSULTA": "LOJAS MM", "TELEFONE_CLIENTE": "---", "CIDADE_UF_CLIENTE": "---" },
{ "DATA_CONSULTA": "21/01/2026", "HORA_CONSULTA": "---", "CLIENTE_CONSULTA": "SUPERSIM EMPRESTIMOS", "TELEFONE_CLIENTE": "---", "CIDADE_UF_CLIENTE": "---" },
{ "DATA_CONSULTA": "14/01/2026", "HORA_CONSULTA": "---", "CLIENTE_CONSULTA": "MS-IQR/AGROPECUARIA CAMPO VERDE", "TELEFONE_CLIENTE": "---", "CIDADE_UF_CLIENTE": "---" },
{ "DATA_CONSULTA": "14/01/2026", "HORA_CONSULTA": "---", "CLIENTE_CONSULTA": "SP-FCA/CONSORCIO LUIZA", "TELEFONE_CLIENTE": "---", "CIDADE_UF_CLIENTE": "---" },
{ "DATA_CONSULTA": "13/01/2026", "HORA_CONSULTA": "---", "CLIENTE_CONSULTA": "CELEIRO SERVICOS FINANCEIROS LTDA", "TELEFONE_CLIENTE": "---", "CIDADE_UF_CLIENTE": "---" },
{ "DATA_CONSULTA": "13/01/2026", "HORA_CONSULTA": "---", "CLIENTE_CONSULTA": "MESSER GASES LTDA", "TELEFONE_CLIENTE": "---", "CIDADE_UF_CLIENTE": "---" },
{ "DATA_CONSULTA": "06/01/2026", "HORA_CONSULTA": "---", "CLIENTE_CONSULTA": "BANCO DAYCOVAL - VEICULOS", "TELEFONE_CLIENTE": "---", "CIDADE_UF_CLIENTE": "---" },
{ "DATA_CONSULTA": "22/12/2025", "HORA_CONSULTA": "---", "CLIENTE_CONSULTA": "CELEIRO SERVICOS FINANCEIROS LTDA", "TELEFONE_CLIENTE": "---", "CIDADE_UF_CLIENTE": "---" },
{ "DATA_CONSULTA": "18/12/2025", "HORA_CONSULTA": "---", "CLIENTE_CONSULTA": "CELEIRO SERVICOS FINANCEIROS LTDA", "TELEFONE_CLIENTE": "---", "CIDADE_UF_CLIENTE": "---" },
{ "DATA_CONSULTA": "17/12/2025", "HORA_CONSULTA": "---", "CLIENTE_CONSULTA": "CELEIRO SERVICOS FINANCEIROS LTDA", "TELEFONE_CLIENTE": "---", "CIDADE_UF_CLIENTE": "---" },
{ "DATA_CONSULTA": "17/12/2025", "HORA_CONSULTA": "---", "CLIENTE_CONSULTA": "MS-MUV/CANDIE S CALCADOS", "TELEFONE_CLIENTE": "---", "CIDADE_UF_CLIENTE": "---" },
{ "DATA_CONSULTA": "17/12/2025", "HORA_CONSULTA": "---", "CLIENTE_CONSULTA": "IMPACTO FACTORING", "TELEFONE_CLIENTE": "---", "CIDADE_UF_CLIENTE": "---" },
{ "DATA_CONSULTA": "11/02/2026", "HORA_CONSULTA": "---", "CLIENTE_CONSULTA": "SP-SNE/ANALISE DE CREDITO", "TELEFONE_CLIENTE": "---", "CIDADE_UF_CLIENTE": "---" },
{ "DATA_CONSULTA": "10/02/2026", "HORA_CONSULTA": "---", "CLIENTE_CONSULTA": "SP-SNE/ANALISE DE CREDITO", "TELEFONE_CLIENTE": "---", "CIDADE_UF_CLIENTE": "---" },
{ "DATA_CONSULTA": "10/02/2026", "HORA_CONSULTA": "---", "CLIENTE_CONSULTA": "CNP CONSORCIO SA ADMINISTRADORA CONS", "TELEFONE_CLIENTE": "---", "CIDADE_UF_CLIENTE": "---" },
{ "DATA_CONSULTA": "31/01/2026", "HORA_CONSULTA": "---", "CLIENTE_CONSULTA": "MG-MBE/BRASIL CARD ADM DE CARTAO CR", "TELEFONE_CLIENTE": "---", "CIDADE_UF_CLIENTE": "---" },
{ "DATA_CONSULTA": "09/01/2026", "HORA_CONSULTA": "---", "CLIENTE_CONSULTA": "LOJAS POMPEIA - CAMAQUA", "TELEFONE_CLIENTE": "---", "CIDADE_UF_CLIENTE": "---" },
{ "DATA_CONSULTA": "07/01/2026", "HORA_CONSULTA": "---", "CLIENTE_CONSULTA": "AFINZ INSTITUICAO DE PAGAMENTO SA", "TELEFONE_CLIENTE": "---", "CIDADE_UF_CLIENTE": "---" },
{ "DATA_CONSULTA": "29/12/2025", "HORA_CONSULTA": "---", "CLIENTE_CONSULTA": "KOIN ADM CARTOES E MEIOS DE PAG", "TELEFONE_CLIENTE": "---", "CIDADE_UF_CLIENTE": "---" },
{ "DATA_CONSULTA": "18/12/2025", "HORA_CONSULTA": "---", "CLIENTE_CONSULTA": "CHEQUE PRE COM TEC DA INF LTDA", "TELEFONE_CLIENTE": "---", "CIDADE_UF_CLIENTE": "---" },
{ "DATA_CONSULTA": "12/12/2025", "HORA_CONSULTA": "---", "CLIENTE_CONSULTA": "TOL ADMINISTRADORA DE CARTOES DE CRE", "TELEFONE_CLIENTE": "---", "CIDADE_UF_CLIENTE": "---" },
{ "DATA_CONSULTA": "09/12/2025", "HORA_CONSULTA": "---", "CLIENTE_CONSULTA": "TOL ADMINISTRADORA DE CARTOES DE CRE", "TELEFONE_CLIENTE": "---", "CIDADE_UF_CLIENTE": "---" },
{ "DATA_CONSULTA": "09/12/2025", "HORA_CONSULTA": "---", "CLIENTE_CONSULTA": "MAGAZINE LUIZA - PACOTE CDC", "TELEFONE_CLIENTE": "---", "CIDADE_UF_CLIENTE": "---" },
{ "DATA_CONSULTA": "03/12/2025", "HORA_CONSULTA": "---", "CLIENTE_CONSULTA": "CHEQUE PRE COM TEC DA INF LTDA", "TELEFONE_CLIENTE": "---", "CIDADE_UF_CLIENTE": "---" },
{ "DATA_CONSULTA": "02/12/2025", "HORA_CONSULTA": "---", "CLIENTE_CONSULTA": "CHEQUE PRE COM TEC DA INF LTDA", "TELEFONE_CLIENTE": "---", "CIDADE_UF_CLIENTE": "---" }
]
},
"RENDA_PRESUMIDA": {
"STATUS_RETORNO": {
"CODIGO": "1",
"DESCRICAO": "CONSULTA CONCLUIDA COM SUCESSO"
},
"DESCRICAO": "RENDA PRESUMIDA",
"FAIXA": "DE R$ 3.001 ATE R$ 4.000",
"RENDA_ANUAL": "",
"MENSAGEM": "RENDA ESTIMADA A PARTIR DE INFORMACOES COMPORTAMENTAIS E CADASTRAIS DO CONSUMIDOR.",
"DATA_REFERENCIA": "",
"VALOR_PRESUMIDO": "",
"DADOS_CADASTRO_POSITIVO": ""
}
},
"VEICULAR": []
}
}
Afirmações conferidas¶
Fatos sobre o repositório que uma rodada de portão já conferiu abrindo o arquivo. O recortar.py leva esta seção junto em qualquer parte, e o revisor da rodada seguinte pode tomá-los como dados em vez de reconferir tudo do zero.
| Afirmação | Onde na spec | Conferida em |
|---|---|---|
Duas regras, e a segunda é a que faz a primeira valer:
- Só entra o que um revisor conferiu — não o que você conferiu ao escrever. O ponto é aproveitar trabalho já pago por uma rodada, não abrir atalho para afirmar sem prova.
- Mexeu na frase da spec, a linha sai da tabela. A afirmação volta a ser conferível. Sem isso a tabela vira o oposto do portão: uma afirmação carimbada como verdadeira porque um dia foi.
Feature sem afirmação sobre o repositório não precisa da seção — apague-a.