Pular para conteúdo

0052 — Catálogo de consultas de crédito configurável, com custo e conciliação (API Full)

Status CONCLUÍDA
Branch spec/0052-consultas-credito-api-full
Criada em 2026-09-09
Apps afetados api · web · e2e
Revisão do problema aprovada (rodada 2)
Revisão da experiência aprovada (rodada 1)
Revisão do detalhamento aprovada (rodada 3)
Revisão da implementação Fases 1 a 4 aprovadas (rodadas 2, 4, 7 e 9); branch aprovada na rodada 12, e as levas seguintes nas rodadas 13 e 14

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.

  1. 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.
  2. A Parte II diz como se usa o que vai existir. Revisada na seção 8 (jornada × viewports).
  3. 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.
  4. 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

A SPEC-0050 (concluída) resolveu a dor original — o analista saindo do Garantora para consultar o Boa Vista por fora — mas deixou, na própria seção 5 daquela spec, "outros bureaus [...] fica para spec futura". Hoje TenantSettings.credit_bureau_type só tem dois valores possíveis, NONE e BOAVISTA_CADASTRO_POSITIVO — é ligado ou desligado, sem meio-termo, um produto só, sempre o mesmo, para toda análise. Isso resolveu o problema de processo, mas criou um segundo problema, de negócio, que a 0050 nunca teve como endereçar: o ADMIN_TENANT não tem nenhum controle sobre custo nem alternativa de cobertura.

Concretamente, hoje:

  • Não existe meio-termo entre "ligado" e "desligado". Toda análise de todo inquilino do tenant recebe exatamente a mesma consulta (Boa Vista Essencial Positivo), custe o que custar ela na tabela da API Full. Não há como pagar menos num caso mais simples nem mais num caso que pede mais dado (por exemplo, exposição bancária via SCR/BACEN, que o Boa Vista não traz).
  • O custo da consulta não aparece em lugar nenhum do Garantora. Nem o preço unitário na tela de Parâmetros, nem o total já gasto no mês, nem uma projeção do que uma configuração vai custar. A única forma de saber quanto está sendo gasto é abrir o próprio painel da API Full, fora do Garantora inteiramente.
  • Não há como conferir se o extrato da API Full bate com o que o Garantora efetivamente consultou. Se a API Full cobrar uma consulta que o Garantora não tem registro (ou o contrário — um registro sem cobrança correspondente), isso só aparece lendo os dois lados manualmente, linha a linha, fora do produto.
  • O preço pago à API Full pode não ser o preço público do produto. Se a garantidora tiver uma condição comercial própria com a API Full, o Garantora não tem onde registrar isso hoje — o custo que a garantidora efetivamente paga não é o custo que o Garantora "sabe". Como isso se estrutura (por faixa, por volume, editável item a item) é desenho de configuração e fica para a Parte III.

O custo de conviver com isso: a garantidora decide ativar (ou não) a única consulta disponível sem visibilidade nenhuma de impacto financeiro mensal, não consegue avaliar se vale pagar mais por um produto com mais dado num perfil de risco maior, e não tem como detectar consumo fora do esperado (falha de idempotência, ou uso da conta da API Full por fora do Garantora) porque não existe conciliação nenhuma entre os dois lados.

2. Quem sente

  • ADMIN_TENANT — é quem hoje liga/desliga credit_bureau_type em Parâmetros e é, perante a garantidora, o responsável pelo orçamento gasto com consultas. Sente a dor na configuração (decide sem ver custo algum) e no fechamento financeiro do mês (precisa ir buscar o gasto real fora do sistema). É o dono desta dor — quem primeiro a sente e quem primeiro precisa da solução.
  • ANALYST — sente em cada análise que decide: recebe sempre o mesmo conjunto fixo de dados (só Boa Vista) e não tem como pedir, pelo sistema, uma consulta adicional quando o caso pede mais profundidade (por exemplo, SCR num inquilino de risco aparentemente maior) — hoje isso exigiria uma nova versão da integração, não uma decisão dele no momento da análise.

Caminho obrigatório, não exceção: toda garantidora que ativa consulta de bureau paga pela mesma consulta única, em toda análise, hoje. Não há operação em produção para medir volume — a plataforma está no ar, mas ainda sem clientes (CLAUDE.md, "Estágio do produto") — então o alcance desta dor se sustenta pelo desenho (é caminho obrigatório da única configuração hoje disponível), não por contagem.

3. A jornada de hoje

Passo a passo, com o que a SPEC-0050 já entrega:

  1. O ADMIN_TENANT abre Parâmetros e ativa credit_bureau_type = BOAVISTA_CADASTRO_POSITIVO, colando o Bearer Token da própria conta na API Full. Nenhuma tela mostra, nesse momento, quanto cada consulta custa nem uma projeção de gasto mensal — a decisão de ativar é tomada sem número nenhum à vista.
  2. A cada Análise de Crédito submetida (DRAFT → SUBMITTED), o Garantora agenda automaticamente a mesma consulta (Boa Vista Essencial Positivo, endpoint scpc-boavista da API Full) para o inquilino principal, via fila em background (arq/Redis) — sem alternativa de produto e sem mostrar custo, nem estimado nem real, em nenhum momento.
  3. O resultado aparece no painel de bureau da análise (BureauInquiryPanel) para o ANALYST decidir — mas não há, ao lado dele, nenhuma informação de quanto essa consulta custou.
  4. Se o ADMIN_TENANT quiser saber quanto a garantidora gastou com consultas num mês, ele precisa sair do Garantora e entrar direto no painel da própria API Full para consultar o extrato de créditos por lá — nenhuma tela do Garantora expõe esse número, nem agregado nem por análise.
  5. Não existe cruzamento entre os dois lados: se o extrato da API Full cobrar algo que o Garantora não tem registro de ter consultado — ou o inverso —, isso só seria percebido comparando os dois manualmente, fora do sistema.
  6. Se o ANALYST avaliar que o Boa Vista não é suficiente para um caso específico (falta exposição bancária, por exemplo), não há como pedir uma consulta adicional pelo sistema — o único jeito seria uma mudança de código, não uma decisão de uso.

Onde quebra: nos passos 1, 4 e 5 — decisão de configuração tomada sem custo à vista, visão de gasto que só existe fora do Garantora, e nenhuma conciliação entre o que o Garantora acha que consultou e o que a API Full efetivamente cobrou.

O que já se tentou: a SPEC-0050 já resolveu a metade operacional desta dor — a consulta em si passou a rodar dentro do Garantora, automática, estruturada e auditável, e o próprio produto que ela integrou (Boa Vista Essencial Positivo, endpoint scpc-boavista) é hoje o único item de um catálogo que, nesta spec, passa a ter mais de um. A 0050 marcou explicitamente "outros bureaus [...] fica para spec futura" — esta é essa spec. Fora dela, não há outro contorno em uso: nenhuma planilha paralela nem processo combinado registra custo de consulta hoje — quem quer esse número, hoje, olha o extrato da API Full por fora.

4. Resultado esperado

O ADMIN_TENANT passa a escolher entre composições prontas de consultas ou montar a própria seleção item a item, vendo o custo unitário de cada uma e o total estimado por análise antes de salvar a configuração. Toda Análise de Crédito passa a mostrar, junto ao resultado de cada consulta, quanto ela custou — estimado no momento, e o valor real depois de conciliado com o extrato da API Full. A garantidora enxerga o gasto do mês com consultas sem sair do Garantora, e consegue ver, num relatório, onde o extrato da API Full diverge do que o sistema registrou.

Como se sabe que resolveu: a decisão sobre bureau deixa de ser binária (ligado/desligado, um produto só) e passa a ter custo visível em cada etapa em que hoje não tem nenhum — na configuração, na análise e no fechamento do mês — sem que o ADMIN_TENANT precise abrir o painel da API Full para saber quanto está gastando.

5. Fora de escopo

  • Decisão automática por resultado de bureau. Como já valia na 0050 (RN8: o resultado é só informativo, não participa de credit_recommendation), a decisão continua manual do ANALYST — o modelo de dados fica pronto para aprovação automática futura, mas esta spec não a implementa.
  • Pessoa jurídica (CNPJ). As consultas cobrem só inquilino pessoa física nesta entrega — mesmo recorte da 0050. Extensão para PJ fica para spec futura.
  • Provedores fora da API Full. Toda consulta desta spec é fornecida pela API Full — não é um conector genérico para qualquer bureau do mercado, é um conjunto fechado de consultas, listado na Parte III.
  • Conector genérico para qualquer produto da API Full. Só as consultas listadas nesta spec — pedir um produto novo da API Full continua exigindo uma spec nova, igual esta é a spec nova que a 0050 previu.
  • Retry automático de consulta com falha. Mantém a decisão da 0050 (RN4b): uma falha fecha o item como FAILED de uma vez, sem reprocessamento automático — dobrar uma tentativa automaticamente dobraria o gasto numa falha que pode ser permanente (CPF inválido, por exemplo). Reexecução continua sendo ação manual, com custo confirmado.
  • Rotação da chave de criptografia do token (TENANT_SECRETS_KEY). Já estava fora de escopo na 0050 (RN3) e continua — trocar a chave invalidaria todos os tokens já cifrados, e não há aqui mecanismo de recifragem em lote.
  • Migração do RBAC para o modelo-alvo de 10 papéis area-scoped (docs/navigation- brief.md). Esta spec usa os papéis reais do código hoje (ADMIN_GLOBAL, ADMIN_TENANT, ANALYST, OPERATOR, VIEWER, AGENCY_USER, AGENCY_MANAGER) — não os dez papéis do modelo-alvo, que dependem de uma spec própria ainda não escrita.
  • Alteração automática de preço a partir de divergência do extrato. A conciliação relata a divergência e propõe; ajustar o preço efetivo continua sendo confirmação humana do ADMIN_TENANT — nunca aplicado sozinho.

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 Nota Veredicto
1 1 1 0 6 Reprovada
2 0 0 1 9 Aprovada

Resultado: aprovada na rodada 2.

Pendências aceitas: nenhuma — o único achado (Minor, nome do produto inconsistente dentro do documento) foi corrigido antes de fechar o portão.


Parte II — A experiência

Não preencher antes da revisão da seção 6 estar aprovada.

7. Jornada pretendida

A feature toca quatro lugares: a seção "Análise de crédito" já existente em Parâmetros (onde hoje mora o Select Nenhuma/Boa Vista), o bloco de bureau já existente na tela de Análise de Crédito (BureauInquiryPanel), uma tela nova de relatório financeiro, e um card novo no Centro de Operações.

  • Ponto de entrada:
  • Configuração — o ADMIN_TENANT já está em Parâmetros (é lá que ativa o Boa Vista hoje); não há tela nova para isto.
  • Resultado na análise — quem já abre a Análise de Crédito para decidir; não há navegação nova, o bloco já existe e passa a mostrar mais.
  • Relatório financeiro — item novo de menu, no grupo "OPERAÇÃO" (ao lado de "Análise de Crédito", "Garantias", "Cobranças"), chamado "Consultas de Crédito".
  • Centro de Operações — quem já abre o painel do ADMIN_TENANT hoje; o card novo entra na mesma grade dos existentes (SummaryCard), sem navegação extra para chegar até ele.
  • Jornada, caso feliz:
  • O ADMIN_TENANT abre Parâmetros → "Análise de crédito" e, em vez do Select binário de hoje, escolhe entre uma composição pronta ou monta a própria seleção — vendo, a cada escolha, o custo unitário de cada consulta e o total estimado por análise, antes de salvar.
  • Ao submeter uma Análise de Crédito (como já faz hoje), o sistema dispara, em background, as consultas da composição ativa — sem pedir nada a mais de quem submete.
  • O ANALYST abre a análise para decidir e vê, no lugar do painel único de hoje, um grupo de cartões — um por consulta —, cada um com seu resultado e o custo daquela consulta. Se o caso pedir mais dado do que a composição ativa cobre, ele pode pedir uma consulta extra ali mesmo, vendo o preço antes de confirmar.
  • O ADMIN_TENANT abre "Consultas de Crédito" quando quiser saber quanto a garantidora está gastando: vê o gasto do mês, a composição do gasto por consulta, e pode sincronizar com o extrato da API Full para ver se bate.
  • No Centro de Operações, o ADMIN_TENANT já vê, sem entrar em nenhuma tela nova, o gasto do mês com consultas ao lado dos demais números que já acompanha — e clica no card para ir ao relatório completo quando quiser mais detalhe.
  • Custo de uso: nada digitado que o sistema já tenha. O CPF do inquilino, o preço público de cada item do catálogo e a composição ativa do tenant já existem quando a análise é submetida — ninguém redigita nada disso. O único dado novo que uma pessoa informa é, opcionalmente, um preço próprio (quando a garantidora tiver condição comercial diferente da tabela) e a escolha de quais consultas compõem sua seleção.

7.1 Desktop

A. Parâmetros — "Análise de crédito". O Select de hoje (Nenhuma / Boa Vista — Cadastro Positivo) dá lugar a um seletor de composição com três opções — duas prontas e uma personalizada —, no mesmo FormPanel que já existe. Escolhida uma composição pronta, a seção mostra, abaixo do seletor, a lista somente-leitura do que ela inclui: uma linha por consulta (nome, preço unitário efetivo do tenant) e uma linha de total ao final, destacada. Escolhida a personalizada, cada linha vira um item marcável (um Switch por consulta, mesmo padrão de campo booleano já usado no restante da tela de Parâmetros), e o total ao final recalcula a cada toque, sem precisar salvar para ver o novo valor. Cada linha tem, ao lado do preço, uma ação de editar (ícone de lápis) que abre um campo de preço em BRL inline — é como a garantidora registra uma condição comercial diferente da tabela pública; fechar sem confirmar não altera nada. O campo de Bearer Token da API Full continua exatamente onde está hoje, sem mudança — é a mesma credencial para todo o catálogo, não uma por consulta.

B. Análise de Crédito — bloco de resultado. No lugar do painel único de hoje, um grupo de cartões na mesma coluna e na mesma posição em que o painel único aparece hoje (depois do que mostra a renda declarada, antes dos documentos pendentes) — um cartão por consulta da composição ativa naquela análise. Cada cartão segue o mesmo molde visual que o painel de hoje (cabeçalho tingido pelo veredito, selo no canto direito, linhas de rótulo/valor no corpo), e ganha um rodapé novo com o custo daquela consulta — estimado até a conciliação, marcado como "confirmado" depois dela (seção 4 desta parte). Acima do grupo, uma linha de resumo soma o custo de todas as consultas daquela análise. Um botão "Adicionar consulta" abre um diálogo listando as consultas do catálogo que não fazem parte da composição ativa, cada uma com preço, e só dispara a chamada depois de um botão de confirmação explícito que já mostra o valor ("Confirmar consulta — R$ X,XX") — nunca um "Salvar" genérico. Em cada cartão com resultado, um ícone "ver payload bruto" (só para quem tem a permissão correspondente — seção 10) abre um drawer lateral com o JSON completo daquela consulta, mesmo padrão do diálogo de glossário que o painel de hoje já usa.

C. Consultas de Crédito — relatório financeiro (tela nova). Cabeçalho com um filtro de período (mês corrente por padrão) e um botão "Sincronizar agora", com a data/hora da última sincronização ao lado. Abaixo, uma linha de números — gasto estimado do período, gasto real (depois de conciliado) e a projeção mensal — no mesmo estilo dos números de resumo já usados no Centro de Operações. Em seguida, uma tabela com o gasto agrupado por consulta do catálogo — colunas: consulta, quantidade no período, custo estimado, custo real, e a diferença entre os dois; sem ação por linha, é só leitura agregada. Por fim, uma tabela de divergências: lançamentos do extrato da API Full sem uma consulta correspondente registrada no Garantora, e consultas registradas sem lançamento correspondente no extrato — cada linha diz de qual dos dois lados falta o correspondente. As duas tabelas paginam com o ListPagination já padrão do app (apps/web/CLAUDE.md, "toda lista longa, nos dois viewports") — a de divergências em especial pode crescer mês a mês sem limite natural.

D. Centro de Operações — card novo. Um SummaryCard a mais na mesma grade horizontal dos existentes ("Análises de Crédito", "Garantias", …): título "Consultas de crédito", valor principal o gasto do mês corrente em BRL, e uma linha de Chips no detalhe (número de consultas no período, e a variação em relação ao mês anterior). Clicar no card navega para a tela "Consultas de Crédito" (C) — mesmo comportamento de clique que os cards existentes já têm hoje.

7.2 Mobile (375px)

A. Parâmetros. A seção já é uma coluna única no mobile hoje (o FormPanel empilha verticalmente em qualquer largura) — a lista de consultas da composição personalizada segue empilhada, uma linha por consulta com o Switch à direita e o preço logo abaixo do nome (em vez de lado a lado, para não espremer o valor numa coluna estreita). O total final continua sendo a última linha da seção, não fixa na tela — mesmo comportamento do resto da página de Parâmetros, que não usa rodapé fixo em nenhuma seção hoje. A edição de preço por item abre o mesmo campo inline, ocupando a largura toda.

B. Análise de Crédito. Os cartões de resultado seguem sempre abertos, empilhados, sem colapsar — a mesma regra que o painel único já segue hoje (nenhum painel de leitura desta tela vira acordeão; só o padding diminui). O botão "Adicionar consulta" e o ícone "ver payload bruto" viram ação de ícone de 44px sem rótulo — o mesmo ajuste que o botão "Reanalisar" do painel de hoje já faz em 375px (BureauInquiryPanel, hoje texto no desktop e ícone no mobile). O diálogo de "Adicionar consulta" e o drawer de payload bruto abrem em tela cheia, não como modal centralizado — o mesmo ajuste que o diálogo de glossário do painel de hoje já faz nesta largura.

C. Consultas de Crédito. A tabela de gasto por consulta e a de divergências viram RecordCard — um cartão por linha, com consulta/lançamento, valores e a diferença (ou o lado que falta, nas divergências) empilhados dentro do cartão. O filtro de período colapsa em FilterPill dentro de um MobileFilterBar, mesmo padrão das demais listas do app. O botão "Sincronizar agora" vira ação de ícone de 44px no topo (mesmo padrão de ação de topo do mobile já usado em outras telas), com a data da última sincronização abaixo do cabeçalho, sempre visível.

D. Centro de Operações. O card novo é o mesmo componente SummaryCard dos demais, na mesma grade flexível que já quebra linha em telas estreitas — nenhum comportamento novo: ele empilha junto dos outros cards exatamente como eles já empilham hoje.

7.3 Estados

  • Vazio:
  • Parâmetros: sem estado vazio — a composição parte sempre de "Nenhuma" (comportamento de hoje).
  • Análise de Crédito: tenant com composição "Nenhuma" e nenhum CreditCheck ainda nesta análise — o bloco não mostra cartão nenhum, mas não desaparece por completo enquanto a análise seguir em decisão (RN9): o botão "Adicionar consulta" continua ali sozinho, porque RN9 promete a consulta avulsa para qualquer análise nessa condição, independente de o tenant ter automatizado algo. O bloco só desaparece de fato quando não há nem cartão nem o que adicionar — sem credit_checks:rerun, ou depois de a análise já decidida (RN9), quando não sobra ação nenhuma para oferecer.
  • Consultas de Crédito: tenant que nunca ativou consulta nenhuma, ou que ainda não sincronizou — mensagem explicando os dois passos possíveis ("ativar em Parâmetros" / "clicar em Sincronizar agora"), cada um como link para o lugar certo, não só um aviso.
  • Centro de Operações: sem gasto no mês, o card mostra R$ 0,00 — não desaparece, mesmo padrão dos demais cards do painel.
  • Carregando:
  • Parâmetros: a lista de consultas do catálogo (nome e preço efetivo de cada uma) é dado novo, vindo do servidor — enquanto carrega, a seção mostra o mesmo esqueleto que o resto da tela de Parâmetros já usa para campos dependentes de rede; o Select de composição fica desabilitado até a lista chegar, para não deixar marcar um item sem saber o preço.
  • Análise de Crédito: cada cartão em PENDING mostra o selo "consultando" com polling, exatamente como o painel único faz hoje — a diferença é que agora é por cartão: um item pode terminar enquanto outro ainda está PENDING.
  • Consultas de Crédito: esqueleto de carregamento nas tabelas/cartões, mesmo padrão das demais listas do app.
  • Erro:
  • Análise de Crédito: um cartão em FAILED mostra a mesma mensagem genérica que o painel único já mostra hoje, sem código técnico nem nome de fornecedor — e os demais cartões da mesma análise continuam mostrando seu próprio resultado normalmente; a falha de uma consulta não esconde as outras.
  • Parâmetros: falha ao salvar a composição ou o preço — mesmo padrão de erro já usado no resto da tela de Parâmetros (mensagem com o que fazer, sem perder o que já estava preenchido).
  • Consultas de Crédito: falha ao sincronizar com a API Full (token inválido, indisponibilidade) — mensagem dizendo a causa em linguagem direta e a saída ("revisar o token em Parâmetros" quando for esse o caso), sem travar a tela nem esconder o que já tinha sido sincronizado antes.
  • Sem permissão:
  • O bloco de resultado na Análise de Crédito segue a mesma régua de acesso que já governa quem vê a análise hoje — a Parte III (autorização e isolamento) fecha exatamente quem alcança o quê, papel a papel; esta parte não antecipa isso. A ação "ver payload bruto" exige permissão própria, mais restrita que a de ver o bloco.
  • "Consultas de Crédito" (relatório) não aparece no menu para quem não tem a permissão de leitura financeira, e digitar a URL direto leva à mesma tela de acesso negado que o resto do app já usa para rota sem permissão.
  • O card do Centro de Operações não aparece para quem não vê o painel inteiro — mesma regra que já vale para os cards existentes.

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 0 4 9 Aprovada

Resultado: aprovada na rodada 1.

Pendências aceitas: nenhuma — os quatro achados Minor (citação trocada do botão que vira ícone em 375px, estado de carregamento faltante em Parâmetros, paginação não dita nas duas tabelas do relatório, citação de padrão de tabela sem ação correspondente) foram corrigidos 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

Catálogo e composição

  • RN1 — Catálogo global de consultas (CreditCatalogItem). Sem tenant_id, mantido pelo ADMIN_GLOBAL (mesmo padrão de dado de plataforma sem TenantMixin que Role/ Permission já usam hoje — DocumentType não serve de precedente aqui, ele tem TenantMixin e é escopado por tenant). Campos: code (único, ex. QUOD_PF), provider (enum CreditCheckProvider; só APIFULL nesta entrega, campo existe para provedor futuro — fora de escopo, seção 5), endpoint (nullable — ver RN1a), name, description, person_type (enum; só PF selecionável nesta entrega, PJ reservado — fora de escopo), default_price_level_1, default_price_level_2 (Numeric), normalized_fields (JSON, lista dos campos que este item produz — RN7), is_active. Seed com os seis itens do pedido original desta spec — nome, preço Nível 1/2 e o que cada um responde vêm da tabela de preços da própria API Full compartilhada nesse pedido (07/09/2026); é dado de negócio fornecido por quem pediu a spec, não verificado num documento público consultável neste repositório.
  • RN1a — Endpoint confirmado só para um item; os demais ficam A CONFIRMAR NA DOC DA API FULL. O único endpoint verificável hoje é o de BOAVISTA_ESSENCIAL (scpc-boavista), porque a SPEC-0050 já o usa em produção, com o formato de request e parsing confirmados contra uma chamada real (docs/specifications/0050-analise-credito-boavista/specification.md, RN4). Os outros cinco itens (QUOD_PF, DADOS_FINANCEIROS, CADIN, PROTESTO_NACIONAL, SCR_PREMIUM) e o endpoint de extrato de créditos (RN15) não têm formato de request/ resposta confirmado neste repositório — uma busca na documentação pública da API Full (doc.apifull.com.br) durante esta spec não encontrou o contrato desses cinco produtos nem do extrato (o portal de créditos parece viver num painel separado, bank.apifull.com.br, não numa rota REST documentada publicamente). CreditCatalogItem. endpoint fica null para os cinco, e ApiFullClient (RN8b) não os implementa nesta entrega — é [SPEC INCOMPLETA], não bug de implementação: a Fase 1 do plano (seção 14) entrega catálogo, configuração e o item confirmado; os cinco restantes entram como itens is_active=false no seed, ativados um a um assim que o formato de cada um for confirmado — sem migration nova, só um UPDATE de is_active e do endpoint.

Atualização da Fase 4 (2026-09-13). A API Full passou a publicar um contrato OpenAPI (doc.apifull.com.br/openapi.json). Ele confirma o extrato (RN15) e o saldo (RN19), e documenta a requisição dos cinco produtos (/api/pf-quod-consulta, /api/pf-dados- financeiros, /api/cadin, /api/protesto-nacional, /api/scr-premium) — mas a resposta de todos vem num envelope genérico, sem os campos. Ativá-los exige uma chamada real a cada um, que consome crédito; decisão do usuário: fica fora desta spec, os cinco continuam is_active=false.

Atualização de 16/09 — o catálogo inteiro passou a rodar. O usuário autorizou chamadas reais com o CPF (e o CEP) dele, uma por item: /api/pf-dados-financeiros (R$ 1,28), /api/cadin (R$ 0,80), /api/protesto-nacional (R$ 3,42), /api/scr-premium (R$ 5,71) e /api/pf-quod-consulta (R$ 9,79) — o Quod escolhido entre as cinco rotas homônimas por bater com o nome do item no catálogo ("QUOD Consulta - Pessoa Física") e com o preço. Todos os preços cobrados bateram com os defaults do catálogo, e o nome de cada cobrança no extrato virou o statement_label (RN16). Com o formato na mão, os seis itens têm método no ApiFullClient, resultado de sandbox fictício no formato real, e endpoint/is_active preenchidos (migrations d5e2b7c1a9f4 e e6f3c8d2b1a5). Nenhum item do catálogo fica sem endpoint — o estado SKIPPED por ITEM_UNAVAILABLE (RN11) continua valendo para o que a plataforma vier a desligar, ou para um item novo que entre sem formato confirmado. - RN2 — Mutuamente exclusivos. QUOD_PF e BOAVISTA_ESSENCIAL respondem à mesma pergunta (cadastro/score/negativação); uma composição personalizada não pode conter os dois. Validado no service ao salvar PATCH /settings (RN5) — 422 CREDIT_CHECK_MUTUALLY_EXCLUSIVE. Na consulta avulsa (RN9), só conta o item da análise que rodou ou está rodando: um que nasceu SKIPPED nunca trouxe resultado e não impede adicionar o outro (decisão do usuário na revisão da branch — no perfil Completo, o QUOD indisponível travava o Boa Vista). - RN3 — Perfis são dado de plataforma, não de tenant. ECONOMICO (BOAVISTA_ESSENCIAL + DADOS_FINANCEIROS + CADIN) e COMPLETO (QUOD_PF + DADOS_FINANCEIROS + CADIN + SCR_PREMIUM) são seed, numa tabela nova CreditProfileItem (profile, catalog_item_ code) — mesmo desenho do catálogo: sem tenant_id, o ADMIN_TENANT escolhe o perfil, não o conteúdo dele. Não há tela nesta entrega para o ADMIN_GLOBAL editar a composição de um perfil — é seed fixo, editável só por migration futura.

Configuração por tenant

  • RN4 — TenantSettings troca o campo binário por uma composição. O campo credit_bureau_type (NONE/BOAVISTA_CADASTRO_POSITIVO, RN1 da 0050) é removido. Entra credit_check_profile — enum NONE / ECONOMICO / COMPLETO / PERSONALIZADO, default NONE, mesmo padrão de enum-como-String já usado em TenantSettings (native_enum=False). Quando PERSONALIZADO, a seleção do tenant vive numa tabela nova TenantCreditCheckSelection (tenant_id, catalog_item_code) — uma linha por item ativo; trocar de perfil para ECONOMICO/COMPLETO/NONE não apaga a seleção personalizada guardada, só deixa de ser a composição em uso (o PATCH grava as duas coisas juntas: perfil e, se for o caso, a lista). Não há base instalada a preservar (CLAUDE.md, "Estágio do produto") — a migration remove credit_bureau_type sem backfill.
  • RN5 — Preço efetivo por tenant, com vigência (TenantCreditCatalogPrice). Nunca sobrescrito in-place: PATCH /settings com um preço novo para um item insere uma linha nova (tenant_id, catalog_item_code, unit_price, effective_from=now(), created_by) — não faz UPDATE. O preço efetivo de um item, num instante, é o unit_price da linha de effective_from mais recente que não seja futura; sem nenhuma linha, é o default do catálogo (RN1) no credit_check_price_level do tenant (1 ou 2, novo campo em TenantSettings, default 1 — nenhum tenant novo começa com preço negociado). O ADMIN_TENANT vê o histórico de preços de um item (mesma tela de Parâmetros, lista simples por effective_from desc, pelo botão "Histórico" de cada item — GET /settings/credit-check-prices/{code}/history) — é o que mantém coerente o custo estimado de uma análise antiga (RN11). As fases deixaram a lista de fora sem registro; entrou na revisão da branch, por decisão do usuário (seção 18).
  • RN6 — Token e sandbox, reaproveitados da 0050, generalizados de nome. O campo credit_bureau_token (Fernet, TENANT_SECRETS_KEY) é renomeado para credit_check_token — é a mesma credencial para todo o catálogo, não uma por bureau, então o nome não deveria mais carregar "bureau" no singular. O mecanismo de criptografia, o token_configured/token_last_4 write-only e o modo sandbox (RN9b/RN9c da 0050) continuam idênticos — inclusive a variável de ambiente CREDIT_BUREAU_SANDBOX_MODE mantém o nome antigo de propósito: é infraestrutura já no .env do dev-remote e da produção (srv01), e renomear a variável exigiria tocar nos dois ambientes fora deste repositório sem ganho — só o campo Python muda de nome, a variável de ambiente não.
  • RN7 — Campos normalizados, por item. CreditCheck.normalized (JSON) guarda só o que aquele item produz, mapeado assim:
Consulta Campos que preenche
QUOD_PF score, has_negative_records, negative_records_count, negative_records_total_amount, e — quando o serviço de protestos da QUOD responde — has_protests, protests_count, protests_total_amount. Sem score_range: a QUOD devolve segmento e sub-scores, não faixa (16/09)
BOAVISTA_ESSENCIAL score, classification, default_probability_percent, financial_pendencies_count, financial_pendencies_total, protests_count, bounced_checks_count, presumed_income_range — os campos reais que o parser já existente na 0050 produz (_extrair(), credit_bureau_service.py:55-66), não uma abstração nova; a correspondência abstrata inicial desta RN (score_range/has_negative_records/…) não batia com o parser real e foi corrigida aqui na Fase 2, ao implementar ApiFullClient.boavista_essencial() (seção 18)
DADOS_FINANCEIROS presumed_income_range, presumed_assets_range, social_class, purchasing_power — os campos reais de dados.dados_financeiros (renda_estimada_v2, patrimonio_estimado, classe_social, poder_de_compra), todos faixas em texto ("DE 2 A 4 SM", "50K A 100K"), conferidos numa chamada real em 16/09; a correspondência abstrata inicial (presumed_income/presumed_assets) supunha valores em reais, que a API não devolve
CADIN has_federal_debt, federal_debt_count, federal_debt_total (Data.Resumo: TotalDividas, ValorTotalConsolidado)
PROTESTO_NACIONAL has_protests, protests_count, protests_total_amount (CREDCADASTRAL.PROTESTO_SINTETICO) — varredura nacional, pode divergir do que QUOD_PF/BOAVISTA_ESSENCIAL já trazem; os números convivem, um por CreditCheck, nunca mesclados
SCR_PREMIUM bank_exposure_total (a vencer + vencido + prejuízo), bank_exposure_overdue, bank_loss_total, bank_credit_limit (à parte: é crédito disponível, não dívida), bank_institutions_count, score, score_range — do CONSOLIDADO do Banco Central (16/09)

Campo que o item não produz fica ausente do JSON (não null — ausência é "este item não responde isso", null seria "respondeu e o valor é vazio").

Execução das consultas

  • RN8 — Nova entidade CreditCheck, uma por consulta executada, dentro da análise — não mais um resultado único por locatário. Substitui CreditBureauInquiry/os campos credit_bureau_* do Renter (RN6/RN9 da 0050): credit_bureau_status, credit_bureau_data, credit_bureau_requested_at, credit_bureau_queried_at são removidos de Renter sem backfill (mesma justificativa da RN4 — sem base instalada). Campos de CreditCheck: id, tenant_id, credit_analysis_id (FK), renter_id (FK — resolvido no service a partir do LESSEE principal da análise, nunca aceito do corpo, seção 10.3), catalog_item_code (FK), status (enum PENDING/ RUNNING/SUCCESS/FAILED/SKIPPED), requested_by (FK User — quem submeteu a análise, para os itens automáticos, ou quem confirmou a reexecução/adição, RN9), requested_at, completed_at, unit_price_estimated (snapshot do preço efetivo, RN5, no instante do agendamento), unit_price_actual (nullable, RN15), provider_request_id (nullable), raw_response (JSON, nullable — RN13 sobre purga), normalized (JSON, RN7), error_message (nullable), reused_from_id (FK própria, nullable — RN10). Sem restrição de unicidade (analysis, item): uma reexecução (RN9) grava uma linha nova, nunca atualiza a anterior (RN12) — a tela e o relatório sempre leem a mais recente por (credit_analysis_id, catalog_item_code) (requested_at desc), e a anterior permanece para auditoria.
  • RN8a — Disparo na submissão, reaproveitando a fila já existente. Na transição DRAFT → SUBMITTED (mesmo ponto da RN4/RN4b da 0050 — depois da transição já aplicada, dentro da mesma transação do CommitRoute), o service resolve a composição ativa do tenant (RN4) e agenda um job arq por item — não um job só para todo o grupo — para que a paralelização seja real: itens independentes rodam ao mesmo tempo, em workers distintos, sem que a lentidão de um atrase os outros. Cada job cria seu CreditCheck em PENDING antes de ser enfileirado (mesmo SAVEPOINT da RN7 da 0050, por item), e o job só entra na fila depois do commit da requisição — antes dele, o worker procuraria a linha e não a acharia (revisão da branch). Nenhuma infraestrutura nova — mesmo container worker, mesma fila Redis já usada pela 0050.
  • RN8b — ApiFullClient. Um client HTTP por provedor (só APIFULL nesta entrega), com timeout e captura de qualquer erro sem deixar subir exceção (mesma regra da RN7 da 0050 para o client existente) — fecha o CreditCheck como FAILED com motivo genérico. Implementa só o método do item confirmado (BOAVISTA_ESSENCIAL, reaproveitando o parser _extrair() já existente); os cinco métodos restantes ficam com uma implementação que levanta NotImplementedError explícito, nunca chamada em produção porque RN1a mantém esses itens is_active=false no seed.
  • RN9 — Reexecução e consulta avulsa, sempre com custo confirmado antes, e só enquanto a análise ainda está em decisão. Dois endpoints (seção 12): reexecutar um item específico da composição (por exemplo, um que falhou) e adicionar um item que não faz parte da composição ativa naquela análise. Nenhum dos dois roda sem o corpo trazer confirm_cost: true — o preço já foi mostrado na tela antes do clique (Parte II, 7.1.B). Sem essa confirmação, 422. Adicionar um item que violaria a RN2 (mutuamente exclusivo com um já presente naquela análise) é 422 CREDIT_CHECK_MUTUALLY_EXCLUSIVE; adicionar um item já presente e não FAILED é 422 CREDIT_CHECK_ITEM_ALREADY_IN_ PROFILE.

Os dois só valem enquanto status da análise está em SUBMITTED, IN_ANALYSIS ou PENDING — decisão do usuário, respondendo a uma tensão encontrada na implementação (ver Registro, seção 18): uma análise já APPROVED, REJECTED ou CANCELLED não aceita nenhum dos dois, 422 CREDIT_CHECK_ANALYSIS_DECIDED — pedir uma consulta nova para uma decisão que já foi tomada não muda nada, e reexecutar um item de uma análise encerrada geraria custo sem propósito. A regra é uma lista de permissão, não de recusa: uma análise em DRAFT também não aceita, com 422 CREDIT_CHECK_ANALYSIS_NOT_SUBMITTED — a consulta paga ficaria invisível (o campo credit_checks vem null no rascunho) e a submissão dispararia o mesmo item de novo. A leitura (credit_checks, GET .../raw) continua disponível em qualquer status, inclusive depois da decisão — é histórico, e RN13 (auditoria de acesso ao bruto) depende de continuar acessível.

Os dois consultam sempre o LESSEE principal de agora (10.3), e o recusam com 422 CREDIT_CHECK_RENTER_NOT_ELIGIBLE quando ele não é pessoa física com CPF — a mesma condição com que a submissão decide não disparar nada. Se o principal mudou depois da submissão, o cartão antigo continua na análise, dizendo de quem era a consulta (renter_name), e "Reexecutar" consulta o novo (decisão do usuário na revisão da branch; nada é cobrado sozinho na troca).

Os dois endpoints também recusam, com 404 CREDIT_CATALOG_ITEM_NOT_FOUND, o item que o catálogo desligou ou que não tem endpoint confirmado (10.3, RN1a) — o desligamento pela plataforma vale para toda chamada paga, não só para a submissão —, e o worker confere o mesmo no momento de rodar, fechando como FAILED o job que ficou na fila quando o item foi desligado. Na tela, "Reexecutar" e "Adicionar" abrem uma confirmação com o preço de hoje no botão ("Confirmar consulta — R$ 3,23"): o custo mostrado no cartão é o de quando a consulta foi agendada, e o de um reaproveitamento é zero. - RN10 — Idempotência por locatário, generalizada por item, com janela configurável. Mesma lógica da RN9 da 0050 (trava contra reconsulta paga em duplicidade do mesmo CPF), agora por (renter_id, catalog_item_code) em vez de "qualquer resultado do bureau": dentro de credit_check_reuse_window_days (novo campo em TenantSettings, default 30, RN4) desde o último CreditCheck SUCCESS daquele locatário para aquele item, uma nova análise reaproveita o resultado — cria um CreditCheck novo (para registro naquela análise) com reused_from_id apontando para o original, copia normalized dele, unit_price_estimated = 0 (não gera custo) e pula direto para SUCCESS, sem chamar ApiFullClient. FAILED não trava (mesma regra da 0050). Consulta em andamento não é duplicada — a nova espera por ela e reaproveita (decisão do usuário na revisão da branch): se o mesmo locatário já tem uma consulta PENDING/RUNNING daquele item, a nova nasce PENDING com reused_from_id apontando para ela, preço zero e sem entrar na fila; quando a original termina, o worker fecha quem esperava — SUCCESS com o normalized copiado, ou FAILED com um motivo que aponta "Reexecutar" (a falha da original não vira cobrança automática de outra) — ou, se a original falhou porque o item foi desligado, com o mesmo motivo dela, sem a saída. Só se espera por quem ainda vai terminar: a original pedida há mais de 5 minutos (STALE_AFTER, parâmetro técnico — o job tem 30 segundos de limite) não prende ninguém, e a análise nova consulta de novo. A leitura da original trava a linha até a espera ser gravada, para o worker, ao fechar a original, sempre enxergar quem espera. O inverso também vale: se o worker está no meio da chamada (ele segura a linha até gravar o resultado), a submissão aguarda esses segundos e reaproveita o resultado já pronto, em vez de esperar em PENDING. credit_check_reuse_enabled (bool, default True) desliga a trava por completo, por tenant — as duas metades dela. - RN11 — Sem token válido, ou item sem endpoint confirmado (RN1a). A análise segue abrindo normalmente; cada CreditCheck que dependeria do token/endpoint ausente nasce SKIPPED com error_message explicando o motivo ("sem token configurado" / "consulta ainda não disponível"), sem consumir a fila nem custar nada (unit_price_estimated = 0). A tela mostra o aviso com link para Parâmetros — mesmo comportamento descrito na Parte II para o estado "sem token".

Falta de dado exigido pela consulta é o mesmo caminho. DADOS_FINANCEIROS exige o CEP junto do CPF (a API Full recusa sem ele: a estimativa de renda é regional). O CEP sai do cadastro do inquilino; faltando, do imóvel da análise; faltando os dois, a consulta nasce SKIPPED com skip_reason = MISSING_DATA, sem custo, e a tela aponta para o cadastro do inquilino (decisão do usuário, 16/09 — o cadastro de PF passou a ter o campo de CEP, só ele, contra a RN6 da spec 0004, que zerava o endereço inteiro). Pedida à mão (RN9), a mesma falta recusa na cara, com 422 CREDIT_CHECK_CEP_REQUIRED.

O motivo vem estruturado, em skip_reason (TOKEN_MISSING, ITEM_UNAVAILABLE, SUSPENDED — RN20, MISSING_DATA): é por ele, não pelo texto, que a tela decide o link e se oferece "Reexecutar" — sim para token ausente, disjuntor e dado faltando (resolvidos o token, a suspensão ou o cadastro, a consulta pode rodar), não para item indisponível. - RN12 — Imutabilidade. CreditCheck nunca tem raw_response nem normalized reescritos depois de SUCCESS/FAILED/SKIPPED — mesma regra da RN7 da 0050. Uma correção sempre é uma linha nova (RN9), nunca um UPDATE na existente. Uma consulta PENDING/RUNNING há mais de 5 minutos (stalled: true) não vai mais terminar — o worker morreu ou o job se perdeu — e passa a aceitar "Reexecutar", em vez de travar o cartão para sempre; a tela a mostra como "sem resposta". - RN13 — Retenção do raw_response (LGPD), 12 meses. Um job diário (mesmo agendador do job de sincronização, RN15) apaga o raw_response (grava null) de todo CreditCheck com completed_at há mais de 12 meses, preenchendo raw_response_purged_at (novo campo, DateTime, nullable) — o normalized não é tocado, é o que sustenta relatório e auditoria depois da purga. Acesso a read_raw (drawer de payload bruto, ou o endpoint por trás dele) grava entrada de auditoria (AuditLog, action="CREDIT_CHECK_RAW_ACCESS") com entity_id do CreditCheck e user_id de quem acessou — decisão de negócio confirmada para esta spec (12 meses). - RN14 — O resultado continua só informativo. Mantém a RN8 da 0050: nenhum campo de CreditCheck/normalized entra em credit_recommendation (compute_credit_summary). O modelo fica pronto para uma automação futura (é por isso que normalized já é estruturado, não texto livre), mas esta spec não a implementa (seção 5).

Visão financeira e conciliação

  • RN15 — Sincronização do extrato (ProviderStatementEntry). Endpoint manual (POST /credit-finance/sync) e um job diário agendado no mesmo worker arq (6h de São Paulo), ambos chamando StatementSyncService, que pagina GET /api/credits/statement da API Full (até 100 por página, intervalo máximo de 366 dias). Confirmado na Fase 4 pelo contrato público (doc.apifull.com.br/openapi.json, que passou a existir depois da busca desta spec) e por uma chamada real à conta da garantidora: cada transação traz id, created_at (horário de São Paulo, sem fuso), direction (debit/credit), value, balance, consultation (o nome comercial da consulta) e observation — sem CPF e sem identificador da requisição. O parser guarda a transação inteira em raw e é tolerante a campo ausente; o horário é gravado em UTC. Idempotência: unique(tenant_id, external_id) com o id do provedor; sem ele, unique(tenant_id, hash) de (data, descrição, valor, sentido). A janela vai de dois dias antes da última sincronização até hoje (a sobreposição cobre o lançamento atrasado); na primeira, 366 dias. Recargas (credit) são gravadas mas nunca casam nem viram divergência. No modo sandbox, nenhuma chamada é feita: o extrato é montado no formato real a partir das consultas SUCCESS do próprio tenant, cobradas pelo preço estimado.
  • RN16 — Casamento lançamento ↔ CreditCheck. Como o extrato real não traz CPF nem identificador da requisição (RN15), o casamento é por item e por tempo: o nome da consulta no lançamento bate com CreditCatalogItem.statement_label (coluna nova, preenchida para BOAVISTA_ESSENCIAL com "Boa vista Essencial Positivo", o valor visto no extrato real — ativar outro item continua sendo só dado, RN1a), e o lançamento casa com a consulta SUCCESS não reaproveitada (RN10 não chama o provedor), ainda sem lançamento, concluída mais perto dele dentro de 15 minutos (parâmetro técnico). Cada consulta casa com um lançamento só. Casamento bem-sucedido preenche CreditCheck.unit_price_actual e ProviderStatementEntry.matched_credit_check_id.
  • RN17 — Relatório de divergências. Lançamento sem CreditCheck correspondente (a garantidora foi cobrada por algo que o Garantora não tem registro de ter consultado — consumo fora da plataforma, ou falha de idempotência) e CreditCheck SUCCESS sem lançamento correspondente depois de um prazo razoável (7 dias corridos desde completed_at — prazo técnico para dar tempo do lançamento aparecer no extrato, não decisão de negócio) — os dois entram no relatório (seção 12), sem ação automática. Enquanto o tenant nunca sincronizou, a lista é vazia — "consulta sem lançamento" é o estado normal de quem nunca trouxe o extrato, não uma divergência — e a consulta só entra como divergente quando a última sincronização já cobre o momento dela.
  • RN17a — O que conta no gasto e na quantidade. O relatório (summary, by-item) e o card da RN21 somam e contam só as consultas que rodaram ou estão rodando — SUCCESS, PENDING e RUNNING. FAILED e SKIPPED ficam fora dos dois: SKIPPED nunca chamou a API Full, e contar como "16 consultas no período" um item que nunca rodou era o que a primeira versão da tela mostrava. O reaproveitamento (RN10) é SUCCESS com preço zero: entra na quantidade e não soma no gasto. O total de custo da própria análise (credit_checks_total_estimated) segue a mesma regra: a consulta FAILED não entra, e o cartão dela diz "Não entra no custo". "Não conciliado" não é zero: o total real (actual_total) vem null enquanto nenhuma consulta do conjunto foi conciliada, e a tela mostra "—", sem calcular diferença contra um valor que ninguém mediu. Decisão do usuário durante a Fase 3 (Registro, seção 18). A diferença compara o mesmo conjunto: actual_total soma só as consultas conciliadas, então a diferença é contra reconciled_estimated_total — o estimado dessas mesmas consultas —, nunca contra o estimado do período inteiro, que inclui as ainda não conciliadas e mostraria uma economia que não existe (revisão da branch).
  • RN18 — Proposta de ajuste de preço, nunca automática. Quando o mesmo catalog_item_code de um tenant acumula 3 divergências consecutivas entre unit_price_estimated e unit_price_actual (constante do serviço, ajustável sem migration — é limiar operacional, não compromisso contratual, ao contrário do prazo de retenção da RN13), o relatório financeiro mostra um aviso sugerindo atualizar o preço efetivo para o valor observado — um botão que pré-preenche o formulário de preço (RN5) com o valor sugerido; salvar continua sendo a mesma ação humana e confirmada de sempre, nunca aplicado sozinho.
  • RN26 — O preço segue o que foi cobrado, quando ninguém digitou um (decisão do usuário, 17/09). Todo dia, logo depois da sincronização do extrato (RN15), o preço efetivo de cada item passa a valer o último valor cobrado na conta daquela garantidora — só nos itens em que ninguém definiu preço à mão. A vigência nova entra como qualquer outra (RN5, insert_override), com created_by nulo: é o que a distingue de um preço digitado, no histórico (automatic: true) e na origem do preço (unit_price_source = "STATEMENT_AUTO"). Só grava quando o valor mudou — preço igual não vira vigência nova.

Onde a RN18 continua valendo: no item com preço digitado. Aí a rotina não mexe, e a tela segue sugerindo o valor observado, para a pessoa aplicar ou não. Sem preço digitado, sugerir seria pedir uma decisão que a rotina já toma sozinha.

A API Full não publica tabela de preços em rota nenhuma (conferido no contrato público, 17/09): a fonte é o extrato de créditos, que traz o valor cobrado por consulta. Por isso o acerto é por garantidora — cada conta tem o próprio preço — e nunca mexe no default do catálogo, que é dado de plataforma (RN1). - RN19 — Teste de token. POST /settings/test-credit-check-token faz a chamada mais barata disponível contra a API Full — GET /api/get-balance, o saldo da conta, que não consome crédito (achado na Fase 4 no contrato público; a versão anterior desta regra previa o extrato de um dia, porque não se conhecia chamada mais barata). Sem token no corpo, testa o salvo e grava credit_check_token_validated_at (novo campo TenantSettings) só quando o provedor aceitar — e o limpa quando recusar. Com token no corpo, testa sem salvar nada. Token recusado não é erro HTTP: 200 com validated: false e o motivo. Salvar um token novo limpa a validação anterior. - RN20 — Rate limit / circuit breaker por tenant. Contador de falhas consecutivas do ApiFullClient por tenant, em Redis (mesma dependência que a fila arq já usa — nenhuma infra nova): 5 falhas em 10 minutos abrem o circuito por 15 minutos; enquanto aberto, novo CreditCheck daquele tenant nasce direto SKIPPED (skip_reason = SUSPENDED, RN11) com motivo "consultas suspensas temporariamente após falhas repetidas", sem tentar a chamada — protege contra consumo em loop por bug, sem exigir decisão do usuário porque é parâmetro técnico de proteção, não compromisso de produto.

Card no Centro de Operações

  • RN21. OperationsCenterService (operations_center_service.py) ganha uma agregação nova — gasto e quantidade do mês corrente até hoje (soma, linha a linha, de unit_price_actual quando conciliado, senão unit_price_estimated, pela regra da RN17a) e a variação percentual contra os mesmos dias do mês anterior (do dia 1 até o mesmo dia, limitado ao último dia do mês anterior quando ele é mais curto; null quando aquele trecho não teve gasto) — decisão do usuário na Fase 3: contra o mês anterior inteiro, todo começo de mês mostraria uma queda que não aconteceu — exposta em GET /operations-center/overview sob a mesma permissão já existente (operations_center:read), sem permissão nova: quem já vê o painel continua vendo, e o card usa a mesma agregação que GET /credit-finance/summary (RN15) calcula, evitando duas fontes de verdade para o mesmo número.

Quem vê o campo credit_checks — uma permissão só, checada no service

  • RN22 — credit_checks:read é o portão único do campo credit_checks, checado dentro do service, não no router. GET /credit-analyses/{id} continua gated por credit_analyses:read (permissão que ADMIN_TENANT, ANALYST, OPERATOR, VIEWER, AGENCY_USER e AGENCY_MANAGER já têm hoje, sem mudança) — mas o campo credit_checks só é preenchido quando current_user também tem credit_checks:read. Concedida a ADMIN_TENANT/ANALYST/OPERATOR/VIEWER (10.2); não concedida a AGENCY_USER/ AGENCY_MANAGER nem a ADMIN_GLOBAL. Isso substitui a checagem ad hoc que a 0050 usa hoje para o mesmo propósito — _bureau_inquiry_for (credit_analysis_service.py:1081-1108) devolve None só olhando current_user.partner_agency_id is not None, uma checagem de identidade espalhada no meio do método. Uma permissão nomeada é mais fácil de auditar (aparece em 10.1/10.2, em vez de exigir abrir o service para saber quem vê) e cobre os dois motivos de exclusão com o mesmo mecanismo — o de AGENCY_USER/AGENCY_MANAGER (canal, não paga: RN23) e o de ADMIN_GLOBAL (plataforma, não é dado de negócio do cliente: RN24) — sem duplicar checagem por role em cada lugar novo que esta spec cria (o próprio campo, RN9/RN9 dos endpoints de reexecução, o relatório financeiro).
  • RN23 — AGENCY_USER/AGENCY_MANAGER não têm credit_checks:read, e é por isso que não veem credit_checks. Mesmo resultado que a checagem por partner_agency_id da 0050 já produz hoje (RN22) — o canal não paga a consulta nem intermedeia esse custo (CLAUDE.md, "Domínio"), e o resultado de crédito de um inquilino específico é dado que a 0050 já fechou para este papel após a correção pós-aprovação ("somente a garantidora tem acesso à informação, a imobiliária não tem"). A exclusão de preço em GET /settings (RN5) é regra à parte: aquela é sobre configuração — o tenant já mostra isso à imobiliária hoje —, esta é sobre resultado de crédito de um inquilino.
  • RN24 — ADMIN_GLOBAL também não tem credit_checks:read, mais uma guarda explícita no código contra a fixture de teste. Sem credit_checks:read na subtração de _ROLE_PERMISSIONS["ADMIN_GLOBAL"] (rbac_seed.py:211-217 — esse papel é montado por "todas menos uma lista de exclusão"; sem entrar na subtração, ele a herdaria por omissão), ADMIN_GLOBAL acabaria com a permissão e o campo apareceria — porque GET /credit-analyses/{id} já é alcançável por ele hoje via credit_analyses:read, independentemente desta spec (CreditAnalysisService._get_authorized, credit_analysis_service.py:1021-1035, usa tenant_scope(current_user), que devolve None para is_admin_global, app/services/scoping.py:42-48 — o BaseRepository então não filtra por tenant nenhum, 10.3). Por isso as cinco permissões novas de credit_checks:*/credit_finance:* entram explicitamente nessa subtração (credit_catalog:read/update não entram — são as únicas duas que ADMIN_GLOBAL deve ter, 10.5). E, além da subtração, CreditCheckService/CreditFinanceService recusam explicitamente current_user.is_admin_global em código — porque a fixture de teste do ADMIN_GLOBAL concede todas as permissões sem consultar o mapa do seed (rbac_seed.py:191-196, já registrado ali pelas specs 0040/0048/0049): sem a guarda no código, um teste que usa essa fixture nunca ficaria vermelho antes de uma correção, porque a fixture ignora o próprio mapa que a correção mexeria.
  • RN25 — Preço vem null no GET /settings para quem não paga, sem depender de permissão nova. Diferente de RN22-RN24 (que são sobre o campo credit_checks de uma análise, ausente por permissão), esta é sobre a tela de configuração: AGENCY_USER/AGENCY_MANAGER mantêm tenant_settings:read (concessão de hoje, sem mudança) e continuam vendo quais consultas compõem o perfil ativo do tenant — só os campos unit_price/unit_price_source/credit_check_composition_total_estimate (seção 12) vêm null na resposta para esses dois papéis, em vez do valor real. É mascaramento de valor na serialização — não some a chave do JSON, como credit_bureau_token (que nem tem contrapartida em texto claro na resposta); aqui a chave continua presente, só o conteúdo é substituído — não uma segunda permissão, porque a informação "quais consultas estão ativas" é operacional (a imobiliária já vê parâmetro comercial equivalente, a faixa de negociação da taxa de garantia) e o que fica de fora é só o valor em reais que a garantidora paga por consulta, não a existência da consulta em si.

10. Autorização e isolamento

10.1 Matriz de papéis

Papel Pode Não pode
ADMIN_GLOBAL Ler/editar o catálogo global (credit_catalog:*) — itens, defaults de preço Nível 1/2, is_active. Ler ou rodar CreditCheck de qualquer tenant, ler configuração de tenant, ler relatório financeiro de tenant — é dado de negócio do cliente, não de plataforma (10.5).
ADMIN_TENANT Tudo do próprio tenant: tenant_settings:update (perfil, preço, token — já existia para o Boa Vista, agora cobre o catálogo inteiro), credit_checks:read/rerun/read_raw, credit_finance:read/sync. Ver ou alterar o catálogo global, ver CreditCheck/configuração/finanças de outro tenant.
ANALYST credit_checks:read (com custo), credit_checks:rerun (reexecutar/adicionar, com confirm_cost), credit_checks:read_raw. Mesmo grupo que já tem renters:reanalyze_credit_bureau hoje. Editar a composição/preço/token (tenant_settings:update é só ADMIN_TENANT), ver o relatório financeiro agregado (credit_finance:read), ver ou editar o catálogo global.
OPERATOR credit_checks:read (com custo) e credit_checks:rerun — mantém o que renters:reanalyze_credit_bureau já concede a este papel hoje (rbac_seed.py, linha 313 confirmada no levantamento desta spec). credit_checks:read_raw (payload bruto — reservado a quem decide, ADMIN_TENANT/ANALYST), editar configuração, ver relatório financeiro.
VIEWER credit_checks:read (com custo, leitura pura) e credit_finance:read. Rodar/reexecutar consulta, ver payload bruto, editar configuração.
AGENCY_USER / AGENCY_MANAGER Continua vendo tenant_settings (GET) como já vê hoje — inclusive quais consultas a composição do tenant inclui, sem os campos de preço (RN25). Nenhum dado de CreditCheck — nem resultado, nem status, nem custo: não têm credit_checks:read (RN23), o que faz o campo credit_checks de GET /credit-analyses/{id} vir null, mesmo resultado que bureau_inquiry: null já dá hoje. Também sem credit_checks:rerun, credit_checks:read_raw, credit_finance:read/sync, editar configuração, ver catálogo global.

O negativo mais caro desta matriz — e o mais fácil de esquecer — não é um papel inteiro sem acesso a um endpoint, é um campo escondido dentro de uma resposta que o papel já recebe por outro motivo. Dois mecanismos diferentes, para dois campos diferentes: em GET /settings, unit_price/o total estimado vêm null na resposta para AGENCY_USER/AGENCY_MANAGER mesmo com a permissão de leitura de sempre (RN25 — mascaramento de valor, a chave continua no JSON); em GET /credit-analyses/{id}, o campo credit_checks inteiro vem null porque a permissão que o governa (credit_checks:read) não é concedida a esses dois papéis nem a ADMIN_GLOBAL (RN22-RN24) — aqui é ausência de permissão, não mascaramento.

Desvio deliberado do pedido original, e o porquê: o pedido sugeria OPERATOR e "PARTNER_OPERATOR" fora da visibilidade de consulta. PARTNER_OPERATOR não existe no código — o papel real de imobiliária é AGENCY_USER/AGENCY_MANAGER (AGENCY_MANAGER introduzido na spec 0051). E OPERATOR já tem renters:reanalyze_credit_bureau concedido no rbac_seed.py de hoje — reduzir esse papel agora seria uma regressão de acesso não pedida por ninguém, então esta spec preserva o que já está concedido e aplica a régua de custo só onde o domínio já distingue quem paga (a garantidora) de quem não paga (a imobiliária).

10.2 Permissões

Reaproveitadas, sem mudança de nome:

  • tenant_settings:read / tenant_settings:update — os campos novos de catálogo, perfil, preço e token entram no mesmo endpoint e na mesma permissão que já regem credit_bureau_type/credit_bureau_token hoje (ADMIN_TENANT, ANALYST, OPERATOR, VIEWER, AGENCY_USER, AGENCY_MANAGER leem; só ADMIN_TENANT escreve). Introduzir um recurso credit_config novo, como o pedido original sugeria, fragmentaria uma tela que já é um único blob de configuração — decisão de simplicidade desta spec, não do pedido.
  • operations_center:read — o card novo do Centro de Operações (RN21) não cria permissão: quem já vê o painel hoje continua vendo, com o campo a mais.

Novas, com o seed que cada uma exige (rbac_seed.py):

Permissão ADMIN_GLOBAL ADMIN_TENANT ANALYST OPERATOR VIEWER AGENCY_USER/AGENCY_MANAGER
credit_catalog:read ✓
credit_catalog:update ✓
credit_checks:read ✓ ✓ ✓ ✓
credit_checks:rerun ✓ ✓ ✓
credit_checks:read_raw ✓ ✓
credit_finance:read ✓ ✓
credit_finance:sync ✓

Toda permissão desta tabela é exigida por pelo menos um endpoint ou campo da seção 12 — nenhuma é declarada sem um ponto de checagem correspondente. credit_checks:read é a única checada dentro do service, não como require_permission do router (RN22): GET /credit-analyses/{id} continua exigindo só credit_analyses:read para existir, e credit_checks:read decide, à parte, se o campo credit_checks daquela resposta vem preenchido ou ausente.

10.3 Isolamento por garantidora (tenant)

  • CreditCatalogItem/CreditProfileItem não têm tenant_id — são dado de plataforma (RN1/RN3), e é por isso que só ADMIN_GLOBAL os administra; todo tenant lê o mesmo catálogo.
  • TenantCreditCheckSelection, TenantCreditCatalogPrice, CreditCheck e ProviderStatementEntry têm tenant_id (TenantMixin) e são filtrados automaticamente pelo BaseRepository para quem tem tenant — nenhuma query nova passa tenant_id=None para um chamador que não seja ADMIN_GLOBAL. Isso não cobre ADMIN_GLOBAL: BaseRepository.get_by_id só filtra if tenant_id and hasattr(...) (apps/api/app/repositories/base.py:51-56), e ADMIN_GLOBAL.tenant_id é None — o filtro simplesmente não roda. É por isso que RN24 exige a guarda de código explícita nos services novos, não confiar no repositório para isso.
  • renter_id em CreditCheck é resolvido no service, nunca aceito do corpo. O endpoint de reexecução/adição (RN9) recebe só catalog_item_code; o locatário é sempre o LESSEE principal já vinculado à CreditAnalysis do path — o mesmo padrão que a 0050 já usa para _bureau_inquiry_for.
  • credit_analysis_id do path é resolvido no escopo do tenant do solicitante antes de qualquer leitura ou escrita — análise de outro tenant responde 404, nunca 403 (mesma regra da RN4 da 0034), nos três endpoints novos de credit_checks (seção 12).
  • catalog_item_code do corpo é validado contra o catálogo ativo — vale para os caminhos onde a pessoa escolhe o código explicitamente: PATCH /settings com perfil PERSONALIZADO (RN4) e os dois endpoints de reexecução/adição (RN9). Nesses caminhos, código inexistente ou is_active=false é 404 (CREDIT_CATALOG_ITEM_NOT_FOUND), nunca 422 — mesmo raciocínio do "registro alheio responde 404": um código que não existe não deveria distinguir "existe mas está desligado" de "nunca existiu". Não vale para os itens que um perfil fixo (ECONOMICO/COMPLETO) resolve sozinho via CreditProfileItem (RN3): esses podem incluir item is_active=false (RN1a — é o caso hoje de DADOS_FINANCEIROS/CADIN em ambos os perfis), e o resultado é o CreditCheck nascer SKIPPED (RN11), não um erro — ninguém escolheu esse código explicitamente para a validação recusar.

10.4 Isolamento por imobiliária

AGENCY_USER/AGENCY_MANAGER continuam alcançando a CreditAnalysis em si só através da própria carteira (partner_agency_id — vínculo derivado da análise, nunca aceito do corpo, mesmo padrão de hoje) — nenhum endpoint novo desta spec aceita partner_agency_id no corpo. Diferente do restante do dado da análise, CreditCheck não segue esse isolamento por imobiliária — segue exclusão total, independente de qual imobiliária é: mesmo dentro de uma análise da própria carteira, este papel não vê nenhum CreditCheck (RN23), porque não tem credit_checks:read. Não é uma variação de "análise de outra imobiliária responde diferente" — é a mesma exclusão que já existe hoje para bureau_inquiry, generalizada para o campo novo. A única exceção de campo (não de alcance) é GET /settings: aí AGENCY_USER/AGENCY_MANAGER seguem vendo a configuração, só sem o preço (RN25) — configuração do tenant não é a mesma coisa que resultado de crédito de um inquilino específico.

10.5 Plataforma × cliente (SaaS admin)

  • ADMIN_GLOBAL e o catálogo (credit_catalog:*): é dado de plataforma — o mesmo catálogo para todas as garantidoras, sem filtro de tenant, mesmo desenho tenant-less já usado por Role/Permission. ADMIN_GLOBAL não tem nenhuma das cinco permissões novas de credit_checks:*/credit_finance:* (elas entram na subtração explícita de _ROLE_PERMISSIONS["ADMIN_GLOBAL"], RN24) — são dado de negócio do cliente (resultado de crédito de um inquilino, gasto de uma garantidora específica), e ADMIN_GLOBAL não tem filtro de tenant no BaseRepository (10.3): se qualquer endpoint novo aceitasse esse papel, ele veria o dado de todos os tenants de uma vez. A exceção que a ausência de permissão sozinha não cobre é GET /credit-analyses/{id}, que ADMIN_GLOBAL já pode chamar por uma permissão que não é desta spec (credit_analyses:read) — é por isso que RN24 soma uma guarda de código explícita (is_admin_global) à ausência de credit_checks:read, em vez de confiar só na permissão para o campo embutido nessa resposta específica. Por isso nenhum dos dois recursos novos aparece na coluna ADMIN_GLOBAL da matriz (10.1).
  • A tela do SaaS admin (/app/admin/*) ganha uma tela nova, de manutenção do catálogo (seção 11), mas ela mostra só os seis itens do catálogo e seus preços default — nunca um número de gasto de uma garantidora específica. Se uma visão agregada "gasto total da plataforma com consultas" fizer sentido no futuro, é uma contagem por garantidora, não o detalhe de qual inquilino foi consultado — e não faz parte desta entrega (seção 5, "fora de escopo" cobre a extensão a novos provedores; a ausência desta visão agregada de plataforma é decisão implícita de manter o SaaS admin restrito ao que ele já mostra hoje: estrutura da plataforma, não operação do cliente).

11. Impacto por app

apps/api

  • Modelos novos:
  • app/models/credit_catalog_item.py — CreditCatalogItem (RN1), sem TenantMixin.
  • app/models/credit_profile_item.py — CreditProfileItem (RN3), sem TenantMixin, seed fixo.
  • app/models/tenant_credit_check_selection.py — TenantCreditCheckSelection (RN4).
  • app/models/tenant_credit_catalog_price.py — TenantCreditCatalogPrice (RN5).
  • app/models/credit_check.py — CreditCheck (RN8), com CreditCheckStatus (enum).
  • app/models/provider_statement_entry.py — ProviderStatementEntry (RN15).
  • Modelos alterados:
  • app/models/tenant_settings.py — remove credit_bureau_type; adiciona credit_check_profile, credit_check_price_level, credit_check_token (renomeado de credit_bureau_token), credit_check_reuse_enabled, credit_check_reuse_window_days, credit_check_token_validated_at (RN4, RN6, RN10, RN19).
  • app/models/renter.py — remove credit_bureau_status, credit_bureau_data, credit_bureau_requested_at, credit_bureau_queried_at (RN8).
  • Migration Alembic (uma, add_credit_check_catalog): cria as seis tabelas novas; altera tenant_settings (drop de uma coluna, adição de cinco); altera renters (drop de quatro colunas); popula credit_catalog_items e credit_profile_items via op.bulk_insert no próprio upgrade() (mesmo padrão que o seed de RBAC já usa fora de migration, mas aqui é dado de catálogo, não de permissão — entra na migration porque o app depende dele existir para a Fase 1 funcionar, RN1a). downgrade() desfaz as seis tabelas, devolve credit_bureau_type a tenant_settings (default NONE) e as quatro colunas a renters (nullable, sem tentar recuperar dado — não há para recuperar, RN4).
  • Endpoints: seção 12.
  • Regras no service:
  • TenantSettingsService — ganha a validação de RN2 (mutuamente exclusivos), RN4 (perfil ⇄ seleção), RN5 (inserir preço novo, nunca UPDATE) — mesmo arquivo que já valida credit_bureau_type != NONE exige token (RN2 da 0050), estendido para os perfis novos.
  • CreditCheckService (novo) — RN8, RN8a, RN9, RN10, RN11, RN12: cria/lê CreditCheck, resolve renter_id no escopo (nunca do corpo), aplica a trava de idempotência, monta a resposta com os campos de custo condicionados à permissão do chamador (10.1).
  • ApiFullClient (novo, app/services/credit_check_provider/api_full_client.py) — RN8b, um método por item do catálogo; só boavista_essencial() implementado, os outros cinco levantam NotImplementedError (nunca alcançados enquanto is_active for false, RN1a).
  • Worker arq — um job novo, run_credit_check(credit_check_id), chamado uma vez por item na submissão (RN8a) e pelos endpoints de reexecução/adição (RN9); e um segundo job, agendado diariamente, que roda RN13 (purga) e RN15 (sincronização automática).
  • StatementSyncService (novo) — RN15, RN16, RN17, RN18.
  • CreditFinanceService (novo) — agrega os números de GET /credit-finance/* (RN15, RN17) e alimenta app/services/operations_center_service.py (RN21) sem duplicar a query.
  • Fase 4, módulos novos: app/services/credit_check_provider/api_full_account.py (extrato e saldo — as chamadas de conta, que lançam erro tipado em vez de nunca lançar como as de consulta), app/services/credit_check_provider/circuit_breaker.py (RN20), app/services/credit_retention_service.py (RN13), app/repositories/provider_statement_repo.py e o job daily_credit_finance_job em app/worker.py.
  • Revisão da branch: CreditCheck.skip_reason (enum CreditCheckSkipReason, na migration da Fase 4); CreditCheckService.list_for_renter e a rota GET /renters/{id}/credit-checks; GET /settings/credit-check-prices/{code}/history em tenant_settings.py; a espera da RN10 (CreditCheckRepository.in_progress_for_renter_item e waiting_on, e o fechamento no worker); STALE_AFTER e CreditCheck.stalled (RN12); o passo depois do commit em app/core/transaction.py (depois_do_commit), que CreditCheckService usa para agendar o job (RN8a).
  • RN26 (17/09): CreditFinanceService.atualizar_precos_pelo_extrato (chamada pelo daily_credit_finance_job, por garantidora, logo depois da sincronização), TenantCreditCatalogPriceRepository.tem_preco_digitado, e a origem STATEMENT_AUTO em CreditCatalogService.effective_price.
  • Dados Financeiros (16/09): ApiFullClient.dados_financeiros() e _extrair_dados_financeiros(); app/services/credit_check_requisitos.py (de onde sai o CEP, usado pelo service e pelo worker); CreditCheckSkipReason.MISSING_DATA; RenterService._enforce_address_rule guarda o cep do PF; a migration d5e2b7c1a9f4 ativa o item; e o resultado de sandbox credit_bureau_sandbox/apifull_dados_financeiros_sucesso.json (fictício, no formato real).
  • app/db/rbac_seed.py — ganha as sete permissões novas da tabela de 10.2 (credit_catalog:read/update, credit_checks:read/rerun/read_raw, credit_finance:read/sync), mapeadas por papel conforme a matriz de 10.1. As cinco de credit_checks:*/credit_finance:* entram na subtração de ADMIN_GLOBAL (_ROLE_PERMISSIONS["ADMIN_GLOBAL"], rbac_seed.py:211-217) — esse papel é montado por "todas menos uma lista de exclusão" (padrão já documentado ali pelas specs 0040/ 0048/0049), então sem entrar na subtração ele as herdaria por omissão. credit_catalog: read/update não entra na subtração — são as únicas duas que ADMIN_GLOBAL deve ter (10.5).
  • credit_analysis_service.py — o ponto de disparo da RN8a entra na mesma função que já dispara a RN4b da 0050 (a chamada ao bureau na submissão); a versão antiga (_schedule_credit_bureau_inquiry, um bureau só) é substituída pela versão que itera a composição ativa.
  • CreditCheckService, o campo credit_checks e as guardas de RN22-RN24. O método que monta esse campo para GET /credit-analyses/{id} checa current_user.has_permission("credit_checks", "read") antes de preenchê-lo (RN22) — e, além disso, recusa explicitamente current_user.is_admin_global (RN24), porque a fixture de teste do ADMIN_GLOBAL concede todas as permissões sem consultar o mapa do seed (rbac_seed.py:191-196, já registrado ali pelas specs 0040/0048/0049): sem a guarda extra, um teste com essa fixture não ficaria vermelho antes da correção. Os endpoints autônomos de credit_checks:*/credit_finance:* (rerun, adicionar, raw, histórico do inquilino, financeiro) usam require_permission e a mesma guarda no service (403 PLATFORM_ADMIN_CANNOT_ACCESS_CREDIT_CHECKS / PLATFORM_ADMIN_CANNOT_READ_CREDIT_FINANCE), como a RN24 diz. A versão anterior deste parágrafo dispensava a guarda nos autônomos, contradizendo a RN24; a revisão da branch achou o raw protegido só pela subtração do seed, sem teste que a exercitasse, e a contradição foi resolvida a favor da RN24 — com um teste que usa o conjunto real do seed, não a fixture. Isso também corrige a leitura de BaseRepository (10.3): ele não filtra nada para ADMIN_GLOBAL (tenant_id é None, base.py:51-56) — é exatamente por isso que a guarda tem de estar no service, não só no filtro de query.
  • Docstrings/Swagger: todo endpoint novo com summary, description (citando a permissão, seguindo o padrão já usado em tenant_settings.py/credit_analyses.py) e response_description.
  • Testes (apps/api/tests/): parsers (ApiFullClient.boavista_essencial() — reaproveita os fixtures de resposta que a 0050 já tem), cálculo de preço efetivo (RN5: override vigente vs. default vs. nível 1/2), RN2 (mutuamente exclusivos), RN10 (reuso dentro e fora da janela), RN9 (confirm_cost ausente → 422), RN13 (purga não mexe em normalized), matching de conciliação (RN16, com o provider_request_id e sem ele), e os negativos de autorização de cada linha da tabela de 10.2 — inclusive AGENCY_USER/AGENCY_MANAGER recebendo GET /credit-analyses/{id} com o campo credit_checks null (RN23, não é só "não vê a tela", é "o campo não traz dado nenhum no JSON") e ADMIN_GLOBAL recebendo o mesmo campo null mesmo com credit_analyses:read concedido (RN24).

apps/web

  • Páginas/componentes alterados:
  • pages/settings/index.tsx — a seção "Análise de crédito" (linhas 741-820 hoje) troca o Select binário pelo seletor de composição com preço (Parte II, 7.1.A); o campo de token permanece.
  • components/shared/BureauInquiryPanel.tsx — deixa de ser um painel único; vira CreditCheckCard (renomeado, um componente por item) mais um CreditCheckGroup que os agrupa e soma o custo (Parte II, 7.1.B). O testIdPrefix existente (credit- analysis-bureau) é reaproveitado como prefixo de cada CreditCheckCard, com o catalog_item_code como sufixo (ex.: credit-analysis-bureau-boavista_essencial).
  • pages/credit-analyses/show.tsx — troca a montagem de um painel único pelo grupo.
  • pages/operations-center/components/SummaryCards.tsx — um SummaryCard a mais (RN21), clicável só para quem tem credit_finance:read.
  • pages/operations-center/components/SummaryCard.tsx — flex-basis de 160px: sem ela os cards nunca quebravam linha, e o quinto card espremia o painel em 375px (7.2.D supunha a quebra; ver Registro, Fase 3). Reaproveitado nos números de resumo de "Consultas de Crédito".
  • components/shared/MobileFilterBar.tsx e components/shared/FilterBar.tsx — busca opcional no MobileFilterBar e allLabel={null} no FilterPill/FilterSelect, para o filtro de período que sempre tem valor (Registro, Fase 3). Aditivo: quem não passa nada continua igual.
  • Páginas/componentes novos:
  • pages/credit-finance/index.tsx — a tela "Consultas de Crédito" (Parte II, 7.1.C).
  • pages/renters/components/RenterCreditChecks.tsx — as consultas do inquilino, na tela dele (Registro, "Revisão da branch").
  • admin/pages/credit-catalog/ (dentro de /app/admin) — manutenção do catálogo global pelo ADMIN_GLOBAL (10.5) — fora do escopo visual detalhado da Parte II (não é tela da garantidora), mas precisa existir para RN1/RN1a serem operáveis sem acesso direto ao banco.
  • data-testid novos: settings-credit-check-profile-input, settings-credit-check-item-<code>-switch, settings-credit-check-item-<code>-price, settings-credit-check-total, credit-analysis-bureau-<code>-card, credit-analysis-bureau-<code>-cost, credit-analysis-add-check-button, credit-analysis-add-check-dialog, credit-analysis-<code>-raw-button, credit-finance-summary-cards, credit-finance-by-item-table, credit-finance-divergences-table, credit-finance-sync-button, credit-finance-filter-bar, credit-finance-period-filter, credit-finance-actual-total, credit-finance-by-item-pagination, credit-finance-divergences-pagination, credit-finance-error, credit-finance-retry, credit-finance-settings-link, operations-center-credit-checks-card.
  • Sider: módulo novo (credit-finance) — App.tsx ganha o recurso Refine (list: "/credit-finance") e components/navigation/nav-groups.tsx ganha o item em OPERAÇÃO com requerPermissao: "credit_finance:read" (Parte II, ponto de entrada C).
  • Menu e rota por permissão: quem não tem credit_finance:read não vê o item; a rota /credit-finance é protegida no roteador (mesmo mecanismo de rota-por-permissão que as demais telas já usam) — digitar a URL direto sem a permissão leva à tela de acesso negado padrão, não só esconde o menu.
  • Responsividade & PWA: desktop e mobile conforme Parte II, 7.1/7.2. Sem impacto de PWA — não há asset novo para cache nem fluxo offline; a tela financeira exige rede (não teria sentido funcionar offline, é sempre dado vivo do servidor).

apps/e2e

  • Specs novos:
  • credit-check-config.spec.ts — ADMIN_TENANT monta uma composição personalizada, vê o total recalcular, salva; tenta salvar com os dois itens mutuamente exclusivos e recebe o erro (RN2).
  • credit-check-execution.spec.ts — submissão de análise dispara os itens da composição ativa (usando o modo sandbox, RN9b/RN9c da 0050, para não gastar crédito real), painel mostra os cartões com custo; reexecução de um item com confirmação.
  • credit-finance.spec.ts — sincronização manual, tabela de gasto por item, relatório de divergências.
  • Estende o spec e2e existente de Análise de Crédito da 0050 para conferir que AGENCY_USER continua sem ver nenhum resultado de crédito, agora no formato novo (credit_checks: null, RN23) — mesma asserção que já existe para bureau_inquiry: null, só migrada para o campo novo.
  • Seeds (tests/fixtures/seeds/): tenant com composição PERSONALIZADO já montada (para os specs de execução não depender da tela de configuração); um CreditCheck antigo (completed_at há mais de 12 meses) para o spec de purga, se o plano de testes do e2e cobrir isso — do contrário, purga é só unitário (é rotina de fundo, sem tela).

12. Contrato de API

Todo path abaixo é relativo a /api/v1.

Configuração do tenant — estende /settings (RN4, RN5, RN6, RN19)

GET /settings
Authorization: Bearer <token>
Permissão: tenant_settings:read
{
  "credit_check_profile": "PERSONALIZADO",
  "credit_check_price_level": 1,
  "credit_check_token_configured": true,
  "credit_check_token_validated_at": "2026-09-01T10:00:00Z",
  "credit_check_sandbox_active": false,
  "credit_check_reuse_enabled": true,
  "credit_check_reuse_window_days": 30,
  "credit_check_catalog": [
    {
      "code": "BOAVISTA_ESSENCIAL",
      "name": "Boa Vista Essencial Positivo",
      "description": "Cadastro, score, pendências, protestos, cheques, Cenprot",
      "is_active_in_catalog": true,
      "selected": true,
      "profiles": ["ECONOMICO"],
      "unit_price": 2.69,
      "unit_price_source": "TENANT_OVERRIDE"
    }
  ]
}

unit_price/unit_price_source/credit_check_composition_total_estimate vêm null quando quem chama é AGENCY_USER/AGENCY_MANAGER (RN25) — a chave continua no JSON, só o valor é mascarado; o restante do objeto é igual. unit_price_source é TENANT_OVERRIDE (alguém digitou), STATEMENT_AUTO (o último valor cobrado, acertado pela rotina diária — RN26) ou CATALOG_DEFAULT (RN5), para a tela saber se mostra "preço próprio", "último valor cobrado" ou "preço padrão". selected diz a composição salva; profiles, em que perfis prontos o item entra (RN3) — é com ele que a tela mostra os itens e o total de um perfil no momento em que ele é escolhido, antes de salvar.

PATCH /settings
Authorization: Bearer <token>
Permissão: tenant_settings:update  (ADMIN_TENANT)

{
  "credit_check_profile": "PERSONALIZADO",
  "credit_check_selected_items": ["BOAVISTA_ESSENCIAL", "DADOS_FINANCEIROS"],
  "credit_check_price_level": 2,
  "credit_check_price_overrides": [{ "catalog_item_code": "BOAVISTA_ESSENCIAL", "unit_price": 2.50 }],
  "credit_check_token": "novo-token-aqui",
  "credit_check_reuse_enabled": true,
  "credit_check_reuse_window_days": 30
}

Todo campo é opcional — o mesmo padrão parcial que PATCH /settings já segue hoje. Erros: 422 CREDIT_CHECK_MUTUALLY_EXCLUSIVE (RN2), 422 CREDIT_CHECK_PROFILE_REQUIRES_SELECTION (perfil PERSONALIZADO sem nenhum item selecionado), 422 CREDIT_CHECK_PROFILE_REQUIRES_TOKEN (perfil diferente de NONE sem token configurado nem sandbox ativo — mesma regra da RN2 da 0050, generalizada), 404 CREDIT_CATALOG_ITEM_NOT_FOUND (código inexistente em credit_check_selected_items ou credit_check_price_overrides).

POST /settings/test-credit-check-token
Authorization: Bearer <token>
Permissão: tenant_settings:update  (ADMIN_TENANT)

{}
{ "validated": true, "validated_at": "2026-09-09T12:00:00Z" }

{ "credit_check_token": "..." } opcional: testa aquele token sem salvar; omitido, testa o salvo e grava credit_check_token_validated_at. A resposta traz também balance (o saldo da conta, quando aceito), message (o motivo, quando recusado — ainda 200, com validated: false) e sandbox. Permissão: perfil ADMIN_TENANT, o mesmo porteiro de PATCH /settings. Erros: 422 CREDIT_CHECK_TOKEN_NOT_CONFIGURED (nada para testar), 502 CREDIT_CHECK_PROVIDER_UNAVAILABLE. RN19 — a chamada é GET /api/get-balance, que não consome crédito. GET /settings ganha credit_check_token_validated_at.

GET /settings/credit-check-prices/{catalog_item_code}/history
Authorization: Bearer <token>
Permissão: perfil ADMIN_TENANT  (o mesmo porteiro de PATCH /settings)
[
  { "unit_price": 2.90, "effective_from": "2026-09-10T14:00:00Z", "automatic": true },
  { "unit_price": 3.23, "effective_from": "2026-09-01T09:00:00Z", "automatic": false }
]

RN5 — os preços próprios do tenant para o item, do mais recente para o mais antigo; lista vazia enquanto o item usa o default do catálogo. Mesmo porteiro de PATCH /settings (preço é o que a RN25 esconde de quem não paga, e VIEWER/ANALYST também não o editam). Erros: 404 CREDIT_CATALOG_ITEM_NOT_FOUND.

Catálogo global — novo, /credit-catalog (RN1, RN1a; SaaS admin)

GET /credit-catalog
Authorization: Bearer <token>
Permissão: credit_catalog:read  (ADMIN_GLOBAL)
[
  {
    "code": "QUOD_PF", "provider": "APIFULL", "endpoint": null, "person_type": "PF",
    "default_price_level_1": 9.79, "default_price_level_2": 8.16, "is_active": false
  },
  {
    "code": "BOAVISTA_ESSENCIAL", "provider": "APIFULL", "endpoint": "scpc-boavista",
    "person_type": "PF", "default_price_level_1": 3.23, "default_price_level_2": 2.69,
    "is_active": true
  }
]
PATCH /credit-catalog/{code}
Authorization: Bearer <token>
Permissão: credit_catalog:update  (ADMIN_GLOBAL)

{ "default_price_level_1": 10.00, "is_active": true }

Erros: 404 CREDIT_CATALOG_ITEM_NOT_FOUND, 422 CREDIT_CATALOG_ITEM_WITHOUT_ENDPOINT (ativar um item sem endpoint confirmado — a submissão o marcaria SKIPPED e a consulta avulsa o deixaria FAILED, RN1a). Alterar um default não sobrescreve TenantCreditCatalogPrice de nenhum tenant (RN5 é independente de RN1).

Execução das consultas — dentro de /credit-analyses/{id} (RN8, RN9, RN10, RN11)

GET /credit-analyses/{id} (endpoint já existente) ganha o campo credit_checks no lugar do antigo bureau_inquiry (0050):

{
  "credit_checks": [
    {
      "catalog_item_code": "BOAVISTA_ESSENCIAL",
      "status": "SUCCESS",
      "requested_at": "2026-09-09T10:00:00Z",
      "completed_at": "2026-09-09T10:00:04Z",
      "normalized": { "score": 720, "classification": "A", "protests_count": 0 },
      "unit_price_estimated": 2.69,
      "unit_price_actual": null,
      "reused_from_id": null,
      "error_message": null,
      "skip_reason": null,
      "renter_id": "uuid",
      "renter_name": "Maria Souza",
      "stalled": false
    }
  ],
  "credit_checks_total_estimated": 2.69
}

credit_checks e credit_checks_total_estimated vêm null na resposta inteira para AGENCY_USER/AGENCY_MANAGER e para ADMIN_GLOBAL — não é só os campos de custo que somem, é o bloco inteiro que não traz dado (RN22-RN24), mesmo resultado que bureau_inquiry: null já dá hoje para o primeiro grupo. A chave continua presente no JSON (mascaramento de valor, mesmo mecanismo da RN25) — nenhum mecanismo deste repositório remove chave de resposta condicionalmente por papel (grep response_model_exclude no código não encontra precedente); é o model_validate padrão de todo outro campo opcional aqui, e é o que bureau_inquiry: null já fazia. Para todo o resto (ADMIN_TENANT, ANALYST, OPERATOR, VIEWER), o objeto sai como está no exemplo acima, custo incluído.

POST /credit-analyses/{id}/credit-checks/{catalog_item_code}/rerun
Authorization: Bearer <token>
Permissão: credit_checks:rerun

{ "confirm_cost": true }
{ "id": "uuid", "catalog_item_code": "BOAVISTA_ESSENCIAL", "status": "PENDING", "unit_price_estimated": 2.69 }

Erros: 404 (análise de outro tenant, ou catalog_item_code fora da composição — reexecutar um item nunca visto nesta análise não existe, é POST /credit-analyses/{id}/credit-checks que adiciona um novo), 422 CREDIT_CHECK_CONFIRMATION_REQUIRED (sem confirm_cost), 422 CREDIT_CHECK_ALREADY_RUNNING (já PENDING), 422 CREDIT_CHECK_TOKEN_NOT_CONFIGURED, 422 CREDIT_CHECK_ANALYSIS_DECIDED (status APPROVED/REJECTED/CANCELLED, RN9), 422 CREDIT_CHECK_ANALYSIS_NOT_SUBMITTED (análise em DRAFT, RN9), 404 CREDIT_CATALOG_ITEM_NOT_FOUND (item desligado no catálogo ou sem endpoint, RN9/10.3), 403 PLATFORM_ADMIN_CANNOT_ACCESS_CREDIT_CHECKS (RN24). O disjuntor aberto (RN20) não é erro: 200 com a consulta SKIPPED, skip_reason: "SUSPENDED".

skip_reason (em toda consulta do campo credit_checks e nestas respostas) é null fora de SKIPPED, e TOKEN_MISSING, ITEM_UNAVAILABLE, SUSPENDED ou MISSING_DATA nele (RN11). renter_id e renter_name dizem quem foi consultado (renter_name só vem preenchido no campo credit_checks da análise); stalled é true na consulta parada da RN12, e só ela PENDING/RUNNING escapa do 422 CREDIT_CHECK_ALREADY_RUNNING. 422 CREDIT_CHECK_RENTER_NOT_ELIGIBLE quando o principal não é pessoa física com CPF, e 422 CREDIT_CHECK_CEP_REQUIRED quando a consulta exige o CEP e não há nem o do inquilino nem o do imóvel (RN11) — hoje só DADOS_FINANCEIROS.

POST /renters e PATCH /renters/{id} passam a guardar o cep também para PF (os demais campos de endereço seguem zerados para pessoa física, RN6 da spec 0004). É o CEP que a consulta de Dados Financeiros exige.

POST /credit-analyses/{id}/credit-checks
Authorization: Bearer <token>
Permissão: credit_checks:rerun

{ "catalog_item_code": "SCR_PREMIUM", "confirm_cost": true }

Mesma resposta e os mesmos erros do endpoint anterior, mais 422 CREDIT_CHECK_ITEM_ALREADY_IN_PROFILE (já presente e não FAILED) e 422 CREDIT_CHECK_MUTUALLY_EXCLUSIVE (RN2, contra os itens já presentes nesta análise).

GET /credit-analyses/{id}/credit-checks/{catalog_item_code}/raw
Authorization: Bearer <token>
Permissão: credit_checks:read_raw
{ "catalog_item_code": "BOAVISTA_ESSENCIAL", "raw_response": { "...": "..." }, "purged": false }

raw_response: null, "purged": true quando já passou da retenção de 12 meses (RN13) — nunca 404 nesse caso, o registro existe, só o payload já foi apagado. Toda chamada a este endpoint grava AuditLog (RN13). Erros: 404 (análise/item fora do escopo do solicitante, ou item sem CreditCheck ainda), 403 PLATFORM_ADMIN_CANNOT_ACCESS_CREDIT_CHECKS (RN24).

GET /renters/{renter_id}/credit-checks
Authorization: Bearer <token>
Permissão: credit_checks:read
[
  {
    "id": "uuid", "catalog_item_code": "BOAVISTA_ESSENCIAL", "status": "SUCCESS",
    "requested_at": "2026-09-09T10:00:00Z", "completed_at": "2026-09-09T10:00:04Z",
    "normalized": { "score": 720 }, "unit_price_estimated": 2.69, "skip_reason": null,
    "credit_analysis_id": "uuid", "credit_analysis_number": "AC-2026-000123"
  }
]

As consultas do inquilino em todas as análises do tenant, da mais recente para a mais antiga (até 100), cada uma com a análise de origem — é o que a tela do inquilino mostra no lugar do painel de bureau da 0050 (seção 18). O inquilino é resolvido no escopo do usuário: outro tenant, ou outra imobiliária, é 404 — o mesmo de GET /renters/{id}. AGENCY_USER/AGENCY_MANAGER não têm a permissão (RN23, 403) e o ADMIN_GLOBAL recebe 403 PLATFORM_ADMIN_CANNOT_ACCESS_CREDIT_CHECKS (RN24).

Visão financeira — novo, /credit-finance (RN15, RN16, RN17, RN18, RN21)

GET /credit-finance/summary?start_date=2026-09-01&end_date=2026-09-30
Authorization: Bearer <token>
Permissão: credit_finance:read
{
  "estimated_total": 542.30, "actual_total": 498.10, "projected_month": 610.00,
  "last_sync_at": "2026-09-09T06:00:00Z"
}

actual_total é null quando nenhuma consulta do período foi conciliada (RN17a) — e, até a Fase 4, sempre. O mesmo vale para actual_total de cada linha de by-item. Resumo e linhas trazem também reconciled_estimated_total: o estimado das consultas conciliadas, a base com que a tela compara actual_total (RN17a). Nos três GET, start_date depois de end_date é 422 CREDIT_FINANCE_INVALID_PERIOD.

GET /credit-finance/by-item?start_date=2026-09-01&end_date=2026-09-30&page=1&per_page=25
Authorization: Bearer <token>
Permissão: credit_finance:read
{
  "items": [
    { "catalog_item_code": "BOAVISTA_ESSENCIAL", "count": 42, "estimated_total": 112.98, "actual_total": 108.20 }
  ],
  "total": 6, "page": 1, "per_page": 25
}
GET /credit-finance/divergences?start_date=2026-09-01&end_date=2026-09-30&page=1&per_page=25
Authorization: Bearer <token>
Permissão: credit_finance:read
{
  "items": [
    {
      "type": "STATEMENT_WITHOUT_CHECK",
      "provider_statement_entry_id": "uuid",
      "description": "scpc-boavista",
      "amount": 2.69,
      "entry_date": "2026-09-08"
    },
    {
      "type": "CHECK_WITHOUT_STATEMENT",
      "credit_check_id": "uuid",
      "catalog_item_code": "SCR_PREMIUM",
      "unit_price_estimated": 4.76,
      "completed_at": "2026-09-01T10:00:00Z"
    }
  ],
  "total": 2, "page": 1, "per_page": 25
}
POST /credit-finance/sync
Authorization: Bearer <token>
Permissão: credit_finance:sync  (ADMIN_TENANT)

{}
{ "synced_entries": 18, "new_entries": 3, "started_at": "...", "finished_at": "..." }

A resposta real traz também matched_entries (quantos lançamentos casaram com uma consulta, RN16). Erros: 422 CREDIT_CHECK_TOKEN_NOT_CONFIGURED (sem token para consultar o extrato), 422 CREDIT_CHECK_TOKEN_REJECTED (a API Full recusou o token), 502 CREDIT_CHECK_PROVIDER_UNAVAILABLE (API Full fora do ar/timeout durante a sincronização).

GET /credit-finance/summary ganha price_suggestions (RN18) — [{ "catalog_item_code": "BOAVISTA_ESSENCIAL", "current_unit_price": 3.23, "observed_unit_price": 2.90 }] — e last_sync_at passa a ser o fim da última sincronização (TenantSettings.credit_check_last_sync_at, coluna nova).

GET /operations-center/overview (endpoint já existente, RN21) ganha:

{ "credit_checks_spend": { "current_month_total": 542.30, "current_month_count": 42, "variation_percent": 12.4 } }

Migration

add_credit_check_catalog (Alembic) — seção 11 descreve o conteúdo completo (upgrade/downgrade). downgrade completo:

  1. Dropa as seis tabelas novas (credit_catalog_items, credit_profile_items, tenant_credit_check_selections, tenant_credit_catalog_prices, credit_checks, provider_statement_entries).
  2. Em tenant_settings: devolve credit_bureau_type (NONE para todo mundo — não há como recuperar qual composição estava ativa, aceito porque não há base instalada, RN4); renomeia credit_check_token de volta para credit_bureau_token (reverte RN6, não descarta o valor); dropa as cinco colunas que só existem nesta spec — credit_check_profile, credit_check_price_level, credit_check_reuse_enabled, credit_check_reuse_window_days, credit_check_token_validated_at.
  3. Devolve as quatro colunas de bureau a renters (nullable, vazias).

Como ficou na implementação (Registro): a migration saiu em três, uma por fase que precisava de schema — eb1df26c8862 (Fase 1, catálogo), 937f3e75d39d (Fase 2, credit_checks e o corte da 0050) e c4f1a9d2e7b3 (Fase 4: provider_statement_entries, credit_checks.raw_response_purged_at, tenant_settings.credit_check_token_validated_at e credit_check_last_sync_at, credit_catalog_items.statement_label). Cada uma com downgrade próprio, exercitado contra o Postgres do dev-remote.

13. Critérios de aceite

  • [x] CA1. ADMIN_TENANT abre Parâmetros, escolhe o perfil ECONOMICO, salva, e a tela mostra os três itens (BOAVISTA_ESSENCIAL, DADOS_FINANCEIROS, CADIN) com preço unitário e o total somado, sem precisar recarregar a página. O total soma só os itens disponíveis — o "Ainda não disponível" nasce SKIPPED e não custa (decisão do usuário na revisão da branch).
  • [x] CA2. No perfil PERSONALIZADO, marcar QUOD_PF com BOAVISTA_ESSENCIAL já marcado é recusado com 422 CREDIT_CHECK_MUTUALLY_EXCLUSIVE (RN2), e a tela mostra o motivo sem perder a seleção já feita.
  • [x] CA2a. PATCH /settings com perfil PERSONALIZADO e credit_check_selected_ items vazio é recusado com 422 CREDIT_CHECK_PROFILE_REQUIRES_SELECTION.
  • [x] CA3. ADMIN_TENANT edita o preço de BOAVISTA_ESSENCIAL; o histórico de preços mostra as duas linhas (a antiga com sua effective_from, a nova), e uma análise submetida antes da troca continua mostrando o preço estimado antigo (RN5).
  • [x] CA4. Ao submeter uma análise com o perfil ECONOMICO ativo (modo sandbox ligado, para não gastar crédito real), a análise mostra três cartões de consulta — um por item. Hoje (RN1a) BOAVISTA_ESSENCIAL e DADOS_FINANCEIROS evoluem de "consultando" para o resultado com o custo no rodapé — o segundo só com CEP do inquilino ou do imóvel, senão nasce SKIPPED por MISSING_DATA —, e CADIN nasce SKIPPED por falta de endpoint (RN11), com o aviso e sem custo — o critério original presumia os três já confirmados, o que só passa a valer conforme RN1a for sendo resolvida item a item (correção feita na Fase 2, ao implementar o disparo real — ver Registro, seção 18).
  • [x] CA5. Um item em FAILED não esconde os demais cartões da mesma análise, que seguem mostrando seu próprio resultado (RN8, Parte II 7.3).
  • [x] CA6. Reexecutar um item sem marcar a confirmação de custo é recusado com 422 CREDIT_CHECK_CONFIRMATION_REQUIRED; com a confirmação, um novo CreditCheck é criado (a linha anterior permanece intacta, RN12).
  • [x] CA6a. Numa análise APPROVED, REJECTED ou CANCELLED, reexecutar ou adicionar uma consulta é recusado com 422 CREDIT_CHECK_ANALYSIS_DECIDED — nas duas primeiras (SUBMITTED, IN_ANALYSIS, PENDING), continua permitido (RN9). A leitura (credit_checks, GET .../raw) segue disponível em qualquer status.
  • [x] CA7. O mesmo locatário, numa segunda análise dentro de 30 dias da primeira consulta SUCCESS de BOAVISTA_ESSENCIAL, recebe o resultado reaproveitado (reused_from_id preenchido) sem custo e sem chamar ApiFullClient (RN10). Depois de 31 dias, uma nova análise volta a consultar de verdade.
  • [x] CA8. Um tenant sem token configurado submete uma análise com perfil ativo: os CreditCheck nascem SKIPPED com motivo explícito, e a tela mostra o aviso com link para Parâmetros (RN11).
  • [x] CA9. "Consultas de Crédito" mostra o gasto estimado/real/projetado do mês, a tabela por item, e a lista de divergências, todos filtráveis por período; só o que rodou entra no gasto e na quantidade, e o real sem conciliação aparece como "—" (RN17a); "Sincronizar agora" atualiza last_sync_at e a lista de lançamentos.
  • [x] CA10. O Centro de Operações mostra o card "Consultas de crédito" com o gasto do mês e a variação, e clicar nele navega para "Consultas de Crédito".
  • [x] CA11. (mobile, 375px) Os cartões de resultado da análise permanecem sempre abertos e empilhados; o botão "Adicionar consulta" e o ícone de payload bruto viram ação de 44px sem rótulo; a tabela de "Consultas de Crédito" vira RecordCard.
  • [x] CA12. (LGPD) Um CreditCheck com completed_at há mais de 12 meses tem raw_response = null e raw_response_purged_at preenchido depois do job diário rodar; GET .../raw devolve "purged": true sem erro; o normalized permanece intacto.
  • [x] CA13. Acessar GET .../raw grava uma entrada em AuditLog com action="CREDIT_CHECK_RAW_ACCESS" e o user_id de quem acessou.

Negativos de autorização (um por papel afetado, RN/seção 10):

  • [x] CA14. ADMIN_GLOBAL chamando GET /credit-finance/summary recebe 403 (sem credit_finance:read, subtraído explicitamente no seed, RN24). Chamando GET /credit-analyses/{id} de uma análise de qualquer tenant, recebe 200 (permissão que já tinha antes desta spec), mas o campo credit_checks vem null — a guarda de RN24 no service (ausência de credit_checks:read mais o guard explícito contra is_admin_global), não a ausência de permissão no endpoint em si, é o que fecha esse caminho.
  • [x] CA15. AGENCY_USER (e AGENCY_MANAGER) chamando GET /credit-analyses/{id} de uma análise da própria carteira recebe 200 com credit_checks e credit_checks_total_estimated null (RN23) — mesmo resultado que bureau_inquiry: null já dá hoje, não uma versão "sem custo" do bloco. Não é o front escondendo campo: o backend nunca calcula nem envia o dado para este papel.
  • [x] CA16. AGENCY_USER chamando GET /credit-finance/summary ou POST /credit-finance/sync recebe 403 (credit_finance:read/credit_finance:sync ausentes).
  • [x] CA17. OPERATOR chamando GET /credit-analyses/{id}/credit-checks/{code}/raw recebe 403 (credit_checks:read_raw ausente) — mas POST .../rerun com confirm_cost: true recebe 200, porque credit_checks:rerun continua concedido a este papel (10.1, preservando renters:reanalyze_credit_bureau de hoje).
  • [x] CA18. VIEWER chamando qualquer endpoint de reexecução/adição recebe 403 (credit_checks:rerun ausente) — VIEWER só lê.
  • [x] CA19. Uma análise de outro tenant, em qualquer endpoint de credit-checks desta spec, responde 404, nunca 403 (10.3).
  • [x] CA20. ADMIN_TENANT chamando PATCH /credit-catalog/{code} recebe 403 — catálogo global é só ADMIN_GLOBAL (10.5).

14. Plano de implementação

Quatro fases pequenas e entregáveis, cada uma com a ordem obrigatória do monorepo (api → web → e2e) dentro dela — a ordem entre fases é a mesma do pedido original.

Fase 1 — Catálogo, configuração, token e estimativa

  • [x] apps/api — CreditCatalogItem, CreditProfileItem, TenantCreditCheckSelection, TenantCreditCatalogPrice; migration com seed dos seis itens (cinco is_active=false, RN1a). Divergência do planejado: TenantSettings ganhou os campos novos, mas não perdeu credit_bureau_type/credit_bureau_token — ver Registro (seção 18): removê-los antes de a Fase 2 substituir o disparo (RN8a) quebraria o agendamento que já roda em produção.
  • [x] apps/api — GET/PATCH /settings estendidos (RN2, RN4, RN5). Divergência: o catálogo global ficou em GET/PATCH /credit-catalog, sem o prefixo /admin que a spec citava — nenhum outro recurso da API usa esse prefixo (a tela do SaaS admin é que vive em /app/admin/* no frontend, chamando o mesmo /api/v1/...); ver Registro. POST /settings/test-credit-check-token (RN19) segue para a Fase 4 como planejado — depende do mesmo endpoint de extrato que RN15 ainda não tem confirmado.
  • [x] apps/api — testes: preço efetivo (RN5, com histórico e vigência), mutuamente exclusivos (RN2), perfil pronto via CreditProfileItem (RN3), seleção exigida no personalizado (RN4), item inativo recusado com 404 (RN1a/10.3), token exigido por perfil, preço mascarado para AGENCY_USER (RN25), CRUD e negativo de autorização do catálogo global (CA20) — 16 testes novos, suíte cheia sem regressão (revisão de fase, rodada 1, seção 17).
  • [x] apps/web — seção "Consultas de crédito" nova em Parâmetros (Parte II, 7.1.A), adicional ao painel "Análise de crédito" existente, não substituindo-o (mesma divergência acima); tela nova admin/credit-catalog (SaaS admin) — 8 testes novos (4 em settings/index.test.tsx, 4 em CreditCatalogPage.test.tsx).
  • [x] apps/e2e — credit-check-config.spec.ts, 5 testes, verde contra o dev-remote reconstruído.

Fase 2 — Execução e tela de análise

  • [x] apps/api — CreditCheck, ApiFullClient (só boavista_essencial()), o job arq por item (RN8a), reexecução/adição (RN9), idempotência (RN10), SKIPPED sem token (RN11); Renter perde os quatro campos de bureau; GET /credit-analyses/{id} troca bureau_inquiry por credit_checks. Corte completo do mecanismo único da 0050 (credit_bureau_service.py/credit_bureau_schedule.py/credit_bureau_worker_ service.py, renters:reanalyze_credit_bureau, os campos credit_bureau_type/ credit_bureau_token→credit_check_token renomeado, RN6) — divergências e a correção de RN7 registradas na seção 18.
  • [x] apps/api — testes: disparo paralelo, reuso dentro/fora da janela, confirm_cost, imutabilidade (RN12), e os negativos de autorização (403 por papel sem credit_checks: rerun, 404 cross-tenant), a resolução de RN9/CA6a (análise decidida) e a consulta em lote na listagem (rodada 4) — 28 testes em test_credit_check_service.py, mais test_api_full_client.py (11 testes) para o ApiFullClient.
  • [x] apps/web — CreditCheckCard/CreditCheckGroup (Parte II, 7.1.B), diálogo de adicionar consulta, drawer de payload bruto — testes em CreditCheckGroup.test.tsx. O botão "Adicionar consulta"/"Reexecutar" acompanha o status da análise (RN9/CA6a), não a lista de cartões estar vazia — resolução da tensão RN9/7.3 registrada na seção 18, decisão do usuário.
  • [x] apps/e2e — credit-check-execution.spec.ts, verdes contra o dev-remote reconstruído: disparo real com o worker processando o job, reexecução, adição avulsa a partir de composição NONE (RN9/CA6a), exclusão de AGENCY_USER. analise-credito-boavista.spec.ts (spec 0050) removido — todo o mecanismo que ele exercitava foi cortado nesta fase.

Fase 3 — Relatório financeiro e card

  • [x] apps/api — CreditFinanceService, os três endpoints de leitura de /credit-finance (summary, by-item, divergences — RN17 sem a sincronização real ainda, porque ProviderStatementEntry/StatementSyncService/POST .../sync são RN15/RN16 e entram só na Fase 4, junto do endpoint que os usa; até lá, unit_price_actual fica sempre null e o relatório de divergências sempre vazio, não fingindo conciliação que não existe), card de operations-center (RN21)
  • [x] apps/api — testes: RN17a (o que conta e o real null), projeção, RN21 (efetivo, mesmos dias, trava de fim de mês), período invertido, isolamento no summary, no by-item e no card do ADMIN_GLOBAL, negativos de ANALYST/OPERATOR/AGENCY_USER/ AGENCY_MANAGER/ADMIN_GLOBAL — 22 casos em test_credit_finance.py, com "hoje" fixo.
  • [x] apps/web — tela "Consultas de Crédito" (Parte II, 7.1.C/7.2.C/7.3), card no Centro de Operações, item de menu (nav-groups.tsx) — 15 casos em credit-finance/index.test.tsx (desktop e mobile) e 4 em SummaryCards.test.tsx.
  • [x] apps/e2e — credit-finance.spec.ts, sem a parte de sincronização real: CA9, CA10, CA16 e 375px, verdes contra o dev-remote reconstruído.

Fase 4 — Sincronização do extrato e conciliação

  • [x] apps/api — ProviderStatementEntry, StatementSyncService (RN15, RN16), POST /credit-finance/sync, POST /settings/test-credit-check-token (RN19, pelo saldo da conta), job diário agendado no worker, purga LGPD (RN13), divergências reais (RN17) e proposta de ajuste de preço (RN18). A condição do plano — RN15 confirmado — foi cumprida: o contrato público apareceu e o formato foi conferido numa chamada real (Registro).
  • [x] apps/api — RN20 (disjuntor por tenant, contador no Redis), no agendamento (CreditCheckService) e no worker.
  • [x] apps/api — testes: parser contra o formato real, idempotência, paginação, casamento (janela, reaproveitamento, uma consulta por lançamento), divergências dos dois lados e o isolamento, sugestão de preço, purga, teste de token, disjuntor, job diário e os negativos de autorização — 58 casos em test_credit_statement_sync.py.
  • [x] apps/web — "Sincronizar agora" de verdade (só com credit_finance:sync), o resultado e os erros com a saída certa, as sugestões de preço (RN18) levando a Parâmetros com o valor preenchido, e "Testar token" em Parâmetros (RN19).
  • [x] apps/e2e — credit-finance.spec.ts com a sincronização (sandbox) e o VIEWER sem o botão.

Ao fechar cada fase

  • [ ] make preflight
  • [ ] make test e make lint (sem apontamento novo nos arquivos tocados)
  • [ ] Validado no browser em desktop e mobile

15. Decisões em aberto

Nenhuma. As duas decisões que travariam a implementação já foram resolvidas durante esta spec, com quem pediu:

  • Relação com a SPEC-0050: generalizar/substituir a arquitetura single-bureau — decisão tomada antes da Parte I ser escrita, é a base de toda a RN8.
  • Retenção do raw_response (LGPD): 12 meses — decisão tomada antes da Parte III, RN13.

O que ficou pendente de confirmação externa (formato de cinco itens do catálogo e do extrato de créditos) não é decisão em aberto no sentido desta seção — é [SPEC INCOMPLETA] documentado na RN1a, com plano de ativação sem migration nova assim que confirmado, e não bloqueia a Fase 1 nem a Fase 2 (que dependem só do item já confirmado). Bloqueia parte da Fase 4 — documentado ali.

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 1 2 2 4 Reprovada
2 1 3 1 6 Reprovada
3 0 1 0 8 Aprovada

Resultado: aprovada na rodada 3.

Riscos aceitos: nenhum. O único achado da rodada 3 (Important — citação de has_permission como função livre em vez de método de User) foi corrigido 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 Fase 1 (api+web+e2e) 1 4 2 5 Reprovada
2 Fase 1 (api+web+e2e) 0 0 0 9 Aprovada
3 Fase 2 (api+web+e2e) 2 2 3 6 Reprovada
4 Fase 2 (api+web+e2e) 0 1 3 9 Aprovada
5 Fase 3 (api+web+e2e) 0 5 13 7 Reprovada
6 Fase 3 (api+web+e2e) 0 4 8 7 Reprovada
7 Fase 3 (api+web+e2e) 0 0 9 9 Aprovada
8 Fase 4 (api+web+e2e) 1 4 8 6 Reprovada
9 Fase 4 (api+web+e2e) 0 1 7 8 Aprovada
10 Branch inteira 2 8 15 6 Reprovada
11 Branch inteira 1 2 13 6 Reprovada
12 Branch inteira 0 2 9 8 Aprovada
13 Correções depois da rodada 12 0 1 4 8 Aprovada
14 Itens do catálogo liberados (16/09) 0 3 8 8 Aprovada

Resultado: as quatro fases e a branch aprovadas. A rodada 12 aprovou com dois Important abertos, que o revisor pediu para levar ao usuário antes do merge; os dois foram corrigidos por decisão dele, junto com os Minor baratos. Como a correção da fila mexeu na CommitRoute, que vale para toda rota da API, a leva passou por uma revisão de confirmação (rodada 13), que aprovou; o que ela apontou também foi corrigido.

Rodada 13 (confirmação) — achados e correção.

  • Important (T1), corrigido: o teste da fila fora do ar não prendia o commit do FAILED depois do commit da requisição — lia na mesma sessão, que via o flush, e a mutação que tirava o commit passava verde. O teste passou a rodar com a sessão em request.state.db, como em produção, e a afirmar que há um commit depois da recusa da fila; sem ele, fica vermelho.
  • Minor, corrigido (defeito novo da rodada 12): com a original comitada PENDING antes de a fila recusar, outra análise do mesmo locatário podia passar a esperar por ela e ficar presa até parar, porque só o worker fecha quem espera. Quem fecha a original FAILED agora fecha a espera junto (fechar_quem_espera, antes privada do worker).
  • Minor, corrigido: um passo depois do commit que falhasse virava 500 para uma requisição já gravada e impedia os seguintes; agora fica registrado no log e os demais rodam. O contrato do mecanismo ganhou testes em test_transaction_boundary.py (passo depois do commit, passo que falha não derruba os outros, handler que falha não roda passo nenhum, e fora de requisição não há onde agendar).
  • Minor, corrigido: o parágrafo final do glossário citava score, probabilidade e renda presumida em qualquer cartão, e listava só parte do que o selo considera; ficou genérico.
  • Minor aceito: o ORDER BY dos códigos da composição não tem teste — o que ele previne (duas submissões travando consultas em ordem inversa) só existe no Postgres.
  • Evidência: as mutações ficaram vermelhas — sem o commit do FAILED, sem fechar a espera, passo sem try, passo rodando no finally (depois de handler que falhou).
  • CA3: a rodada 12 registrou que o pytest cobria o preço estimado de uma análise antiga depois da troca de preço; o teste não existia. Existe agora (test_preco_novo_nao_muda_o_estimado_de_analise_ja_submetida).

Branch, rodada 12 — achados e correção.

  • Important (RN8a), corrigido — decisão do usuário: o job entrava na fila antes do commit da requisição. O worker lê a fila a cada meio segundo; se pegasse o job nesse intervalo, não achava a linha, saía sem fazer nada, e a consulta ficava "consultando" até parar — capturando, pela espera da RN10, as análises seguintes do mesmo inquilino. Herdado da 0050, mas multiplicado aqui (um job por item, rerun, add). A CommitRoute ganhou um passo "depois do commit" (depois_do_commit), e o agendamento do job passou para ele; a falha da fila, que antes fechava a consulta FAILED dentro da transação, agora a fecha com o próprio commit. Teste com a sessão da requisição em request.state.db, como em produção, afirmando que o commit vem antes da fila na submissão, no add e no rerun.
  • Important (regressão não declarada), corrigido — decisão do usuário: o glossário "O que significam estes números?", que a 0050 pôs no painel a pedido do usuário, sumiu com o cartão novo. Voltou em cada cartão concluído, mostrando só os verbetes dos campos que aquele cartão tem.
  • Minor corrigidos: duas cláusulas da RN10 sem teste que falhasse (a espera nunca é base de outra — terceira análise simultânea; a releitura do sucesso depois da trava — dublando a trava para fechar a original no meio); a tela deixou de oferecer consulta numa análise removida e passou a usar a mesma lista de permitidos do backend; a tela do inquilino diz "Sem resposta" na consulta parada; a ordem dos itens da composição ficou fixa (ORDER BY), para duas submissões do mesmo inquilino travarem as consultas na mesma ordem; docstrings e Swagger que falavam de "ainda não existe", "até a Fase 4", "ausente" e "não validada aqui"; o e2e "CA1/CA3" virou "CA1" (não mexe em preço) e parte de "Nenhuma", para não depender do estado do tenant compartilhado.
  • Minor aceitos: o rerun não aplica a RN2 — só alcançável reexecutando um QUOD SKIPPED depois de adicionar o Boa Vista, e o QUOD está inativo (a RN9 fala da RN2 só no "adicionar"); o cartão ITEM_UNAVAILABLE não tem saída quando a plataforma ativa o item depois (a RN1a não diz o que acontece com as análises abertas); Parâmetros inicia o Personalizado com os itens da composição em uso, não com a seleção guardada, que o GET /settings não expõe (a RN4 a preserva no servidor); uma falha de banco na leitura com trava ainda reverte a submissão (com a ordem fixa, só por erro do banco); a segunda metade do CA3 (análise antiga mantém o preço estimado dela) não tem e2e — o pytest cobre o preço gravado no agendamento.
  • Evidência: as mutações da correção ficaram vermelhas — agendar na hora, rodar o passo antes do commit, a fila fora do ar sem fechar FAILED, sem o filtro de espera, sem a releitura, sem a linha do glossário, glossário com todos os verbetes, análise removida oferecendo reexecução e bruto, e o "Sem resposta" do inquilino.

Branch, rodada 11 — achados e correção. Três pontos eram [SPEC INCOMPLETA] e voltaram ao usuário (Registro, "Revisão da branch"): o que acontece com as consultas quando o principal muda depois da submissão, se o total estimado de Parâmetros soma o item indisponível, e se um item que só ficou SKIPPED conta para a exclusão mútua. O achado 2 nasceu da correção da rodada 10 (a espera da RN10): é interação entre correções da mesma máquina de estados, e a rodada seguinte deve olhar essa máquina de novo.

  • Critical (RN8/10.3): "Reexecutar" consultava o locatário da execução anterior, não o principal de agora — trocado o inquilino depois da submissão, a análise pagava uma consulta sobre alguém que já não fazia parte dela. Passou a usar o principal atual, como o add; o cartão antigo diz de quem era (renter_name). Teste com a troca do principal.
  • Important (RN10, interação com a rodada 10): a espera não tinha limite. Por corrida de commit (a original terminando entre a leitura e o commit da espera) ou por uma original que nunca fecha (worker morto, job perdido), a espera ficava PENDING para sempre e capturava as análises seguintes do mesmo inquilino. Três correções: a leitura da original trava a linha até a espera ser gravada (o worker, ao fechar a original, espera o commit e a enxerga); só se espera por original pedida há menos de 5 minutos; e a consulta parada há mais que isso aceita "Reexecutar" (stalled), na API e na tela. Depois da trava, a leitura do sucesso é refeita, para a original que terminou no meio virar reaproveitamento e não chamada nova. A trava não se prova no SQLite da suíte (ele ignora FOR UPDATE); o limite e a saída têm teste.
  • Important (RN6): a exclusão de test_tenant_settings_credit_bureau.py levou os testes do token write-only e do sandbox. Voltaram, sobre o campo novo: o token nunca volta no PATCH nem no GET, credit_check_token_configured nos dois sentidos, sandbox inativo por padrão, e sandbox que aparece e dispensa o token. O do token longo não voltou: a coluna agora é Text, e o teste não teria como falhar.
  • Pendência que entrou: com a composição "Nenhuma", o campo do token sumia, e é justamente o caso da consulta avulsa. O token saiu de dentro da composição.
  • Minor corrigidos: o total estimado de Parâmetros soma só o que roda (API e tela); o SKIPPED não conta para a exclusão mútua; rerun/add recusam principal sem CPF (422 CREDIT_CHECK_RENTER_NOT_ELIGIBLE), como a submissão; o cartão FAILED não mostra custo nem entra no total da análise; a gaveta do bruto diz quando não há payload ou quando a leitura falhou, em vez de {}, e a consulta reaproveitada não oferece o bruto; o histórico do inquilino ignora análises removidas e abre para o inquilino removido quem tem renters:restore; o link para Parâmetros só aparece para o ADMIN_TENANT (os demais leem o recado); a espera de um item desligado não manda reexecutar; o Swagger do catálogo documenta o 422 de ativação; o teste de divergência com o nome da Fase 3 saiu (duplicava o da Fase 4); a seção 12, o CA20 e o Registro diziam /admin/credit-catalog (o path é /credit-catalog) e o CA17 dizia POST no bruto (é GET).
  • Evidência: cada regra nova da API teve a mutação conferida vermelha (principal atual, elegibilidade, consulta parada no rerun, limite da espera, SKIPPED na exclusão mútua, FAILED no total, motivo da espera de item desligado, análise removida, inquilino removido, total de Parâmetros, sandbox dispensando o token), e as da tela também (link para quem configura, nome do inquilino, consulta parada, custo da falha, gaveta sem payload e com erro, bruto do reaproveitamento, total de Parâmetros).

Branch, rodada 10 — achados e correção. Três pontos pediam decisão e voltaram ao usuário antes de corrigir (Registro, "Revisão da branch"): o que a segunda análise recebe quando a consulta do mesmo item está em andamento (RN10), o histórico de preços que as fases deixaram de fora (RN5) e o painel do inquilino removido sem registro.

  • Critical (RN9): add e rerun recusavam só os status decididos, então uma análise em DRAFT aceitava consulta paga, invisível na tela e disparada de novo na submissão. Virou lista de permissão (SUBMITTED/IN_ANALYSIS/PENDING), com código próprio para o rascunho — CREDIT_CHECK_ANALYSIS_NOT_SUBMITTED, porque ..._DECIDED não descreve um rascunho. Teste de DRAFT nos dois endpoints.
  • Critical (T2, RN24): rerun, add e raw não tinham a guarda do ADMIN_GLOBAL que a RN24 promete, e a seção 11 dispensava — a spec se contradizia. Resolvido a favor da RN24 (guarda no service, também no histórico do inquilino), com dois testes: a guarda nos três endpoints e o conjunto real de permissões que o seed dá ao ADMIN_GLOBAL, não o da fixture. Tirar credit_checks:read_raw da subtração do seed deixa o segundo vermelho.
  • Important (RN5/CA3): o histórico de preços não tinha endpoint nem tela. Entrou GET /settings/credit-check-prices/{code}/history (ADMIN_TENANT) e o botão "Histórico" em cada item de Parâmetros, com negativo de VIEWER e ANALYST.
  • Important (10.3): rerun não conferia se o item continuava ativo, e o worker também não. Os dois endpoints devolvem 404 para item desligado ou sem endpoint; o worker fecha como FAILED o job de um item desligado depois de enfileirado.
  • Important (RN11/RN20): o cartão SKIPPED não tinha saída — resolvido o token, a consulta não podia mais rodar. CreditCheck.skip_reason (coluna nova) diz o motivo; a tela oferece "Reexecutar" para token ausente e disjuntor, e o link certo para cada caso. Resolve junto a pendência 1 da Fase 2 (o link dependia do texto da mensagem).
  • Important (RN9): "Reexecutar" cobrava num clique, confiando num preço mostrado que podia ser o de um reaproveitamento (R$ 0,00) ou o de semanas atrás. "Reexecutar" e "Adicionar" abrem uma confirmação com o preço de hoje no botão.
  • Important (RN17a, tela afirmando dado): "Diferença" comparava o real das consultas conciliadas com o estimado de todas. A API passou a devolver o estimado só das conciliadas (reconciled_estimated_total), e a diferença compara os dois; teste com metade das consultas conciliadas.
  • Important (CA13/RN13): a auditoria do acesso ao bruto não tinha teste. Tem.
  • Important (CA11, 375px): o diálogo "Adicionar consulta" e a gaveta do bruto não tinham como fechar no celular (tela cheia, sem fundo clicável). Os dois ganharam botão de fechar, e os ícones do cartão, 44px no celular.
  • Important (RN10): a consulta em andamento era duplicada. Com a decisão do usuário, a nova espera pela original e reaproveita o resultado; se a original falhar, a que esperava fecha FAILED apontando "Reexecutar". Testes do sucesso, da falha e da trava desligada.
  • Minor corrigidos: o Swagger de /settings e do schema dizia "ausente" onde a RN25 e o código devolvem null; o Swagger de rerun/add ganhou os erros que faltavam; o seed de BOAVISTA_ESSENCIAL passou a gravar os campos reais da RN7 (a migration da Fase 4 corrige o dado, e o downgrade o devolve); "(RN1a)" saiu do texto do cartão; o catálogo não ativa item sem endpoint (422 CREDIT_CATALOG_ITEM_WITHOUT_ENDPOINT) e a tela desabilita o switch; Parâmetros não marca nem deixa marcar item indisponível ao trocar de perfil; o CA15 cobre o AGENCY_MANAGER no pytest; o add do CA19 usa um item ativo, para o teste depender do isolamento; o e2e do CA6 confirma o preço e afirma a linha nova pela troca do id (antes, length >= 1); o finally de credit-check-config.spec.ts devolve a seleção personalizada; os comentários da 0050 em config.py e nos .env.example.
  • Minor aceitos: no sandbox, a tela esconde o campo do token e a execução ainda exige um (afeta só o dev-remote, onde o e2e grava um token de sandbox); com a composição "Nenhuma", o campo do token some junto, embora a consulta avulsa precise dele; o meta-teste de porteiro conta current_user.has_permission( como porteiro, o que hoje é verdade em todos os usos. Das pendências das fases, a 1 foi resolvida acima e as outras oito ficam, pela triagem do revisor.
  • Evidência: as mutações das regras novas na API ficaram vermelhas uma a uma (lista de permissão, guarda do ADMIN_GLOBAL, item desligado no rerun e no worker, auditoria do bruto, espera da RN10, base conciliada, ativação sem endpoint, porteiro do histórico). A migration da Fase 4 foi refeita no Postgres do dev-remote (downgrade → upgrade), conferindo a coluna nova e o dado do catálogo nos dois sentidos.

Defeitos aceitos (Fase 4): nenhum Important aberto. O da rodada 9 foi corrigido antes de fechar, embora a nota já aprovasse: a purga de garantidora quebrava no Postgres assim que o extrato sincronizasse — provider_statement_entries tem FK sem ondelete para credit_checks e para tenants, e delete_tenant_cascade não conhecia a tabela (nem as duas da Fase 1, tenant_credit_check_selections e tenant_credit_catalog_prices). A suíte em SQLite não enxergava, porque não liga as FKs. Reproduzido contra o Postgres do dev-remote — a purga da garantidora de e2e numa transação desfeita no fim, que falhou com ForeignKeyViolationError — e conferido de novo depois da correção, que remove as três tabelas e as consultas às claras, na ordem das FKs. Teste novo prova que as linhas saem. Dos sete Minor, corrigidos: a cláusula "a consulta mais próxima" da RN16, a sugestão que some quando o preço já foi ajustado e o sucesso zerando a contagem no worker ganharam teste (as três mutações ficaram vermelhas); o aviso de Parâmetros cobre também o item fora de uma composição pronta; o texto da sugestão compara com o estimado de cada consulta, e não com o preço de hoje. Pendências aceitas, para a revisão da branch: o filtro de tenant de known_keys sem teste que fique vermelho (não há vazamento possível — o id da API Full é global, e sem o filtro B só deixaria de gravar um lançamento dela); cada sincronização carrega os ids gravados e os débitos soltos do tenant inteiro, o que é pequeno com o volume de hoje.

Fase 4, rodada 8 — achados e correção:

  • Critical (T2, isolamento): o caminho que escreve na conciliação não tinha teste de tenant. O repositório do extrato filtra o tenant à mão em cada consulta, e só uma delas tinha negativo — o teste de isolamento sincronizava uma página vazia. Corrigido com dois testes: duas garantidoras com consulta e cobrança do mesmo item a três minutos uma da outra, sincronizando nas duas ordens, conferindo casamento, custo real, divergências e sugestões de cada uma; e o sandbox de uma não cobrando a consulta da outra. Mutação por mutação, cada filtro de tenant removido deixa um dos dois vermelho — e o arranjo do primeiro teste precisou de uma consulta conciliada da própria B para o filtro da RN18 deixar de ser redundante com o da lista de itens.
  • Important (A1, tela afirmando dado): a sugestão de preço dizia que "as três últimas cobranças vieram a R$ 2,90"; a API só garante que as três divergiram do estimado e que a mais recente foi R$ 2,90. O texto passou a dizer exatamente isso.
  • Important (defeito): o hash de deduplicação valia para todo lançamento, e duas cobranças do mesmo item e preço no mesmo segundo (duas análises submetidas juntas), com ids diferentes, viravam uma só. O hash passou a ser só o fallback do lançamento sem id (coluna anulável; NULL não conflita no unique), como a RN15 diz.
  • Important (T1, RN20): o disjuntor no rerun e no add, e a janela de 10 minutos do armazenamento de produção, não tinham teste. Dois testes de endpoint e um do RedisStore com um Redis falso.
  • Important (T1): cláusulas das RN15–RN17 sem teste que falhasse — a janela da sincronização, "coberta pela última sincronização", recarga fora das divergências e do casamento, consulta sem custo fora de "consulta sem lançamento", e a consulta já casada não casar de novo. Um teste para cada; as mutações correspondentes ficaram vermelhas.
  • Minor, corrigidos: docstring do schema ainda dizia "Fase 3"; negativos de AGENCY_USER na sincronização e de ANALYST/OPERATOR no teste de token; duas sincronizações simultâneas do mesmo tenant podiam responder 500 no unique — cada lançamento grava num SAVEPOINT e o repetido é pulado; a sugestão de preço carregava todo o histórico conciliado do tenant — agora é uma consulta com limite por item; o vazio das divergências ganhou o botão "Sincronizar agora" (7.3 pede ação, não aviso); Parâmetros avisa quando a sugestão chega com a composição em "Nenhuma"; no sandbox, o teste de token não grava mais "aceito pela API Full".
  • Minor, sem mudança: as divergências paginam em memória, dentro do período pedido — o volume de um mês é pequeno; o e2e deixa o tenant compartilhado sincronizado, o que não se desfaz pela API, e o próprio spec deixou de afirmar o estado anterior à sincronização.

Defeitos aceitos (Fase 3): nenhum Important aberto. Dos nove Minor da rodada 7, corrigidos antes de fechar: o teste de troca de mês com nome que prometia mais do que provava (renomeado para o que prova), o inventário de telas do apps/web/CLAUDE.md sem "Consultas de Crédito" e "Catálogo de Consultas", o Swagger prometendo ao ADMIN_GLOBAL um código de erro que a produção não devolve (em produção é FORBIDDEN, pela permissão), docstrings de classe e método em CreditFinanceService/CreditFinanceRepository, e a lista de meses calculada no import do módulo. Pendências aceitas, para a revisão da branch: o card mostra "R$ 0,00 · 0 consultas" quando o overview falha na primeira carga — herança do Painel, que faz o mesmo nos outros quatro cards; o teste do card em test_operations_center.py grava um CreditCheck com renter_id que não existe (passa porque o SQLite da suíte não liga PRAGMA foreign_keys); dois ramos defensivos sem teste (período que ainda não começou e o dezembro de _ultimo_dia_do_mes), inalcançáveis pela tela; e os negativos de papel testados em um endpoint cada, com o mesmo porteiro nos três.

Defeitos aceitos (Fase 2): nenhum aberto — o único Important da rodada 4 (consulta N+1 em CreditAnalysisService.search()) foi corrigido antes de fechar a fase, não deixado como pendência (seção 18 registra a correção). Os três Minor da rodada 4 não bloquearam a aprovação: o acoplamento do link de Parâmetros ao texto de error_message (em vez de um código estruturado), a nomenclatura credit-analysis-bureau-* divergindo do texto literal da seção 11 (mantida de propósito — é o testIdPrefix que a própria seção 11 manda reaproveitar), e uma lacuna do artefato de diff da rodada 4 (não incluía as exclusões de arquivo, só as inclusões/alterações) — conferida à mão contra o repositório, sem achado.

Fase 3, rodada 6 — achados e correção:

  • Important (A1): com a carga falhando, a tela ainda afirmava dado — "R$ 0,00", "nada conciliado", "nenhuma consulta no período", "sem extrato" — sob o alerta de erro; e trocar para um mês que falhava deixava os números do mês anterior sob o rótulo novo. Corrigido: com erro, nenhuma seção de dado aparece; trocar de mês limpa o que era do anterior. Dois casos de tela novos, vistos vermelhos antes da correção.
  • Important (T1): testes da API que ficavam vermelhos entre 21h e meia-noite (São Paulo) do último dia de cada mês — o SQLite da suíte descarta o fuso de DateTime(timezone= True) e compara texto, e os testes gravavam requested_at como "agora" em UTC. Corrigido: "hoje" fixo em todo o test_credit_finance.py (fixture autouse) e consultas ao meio-dia UTC de dia conhecido; o mesmo no teste do Centro de Operações.
  • Important (T2): o card do ADMIN_GLOBAL não tinha teste de isolamento — o zero dele depende do filtro incondicional de tenant. Teste novo; red-green trocando o filtro pelo if tenant_id: do BaseRepository.
  • Important (T1): a trava de fim de mês da RN21 (min com o último dia do mês anterior) não tinha teste; sem ela, o painel responderia 500 nos dias 29 a 31. Teste novo em 31/03 depois de fevereiro; red-green.
  • Minor, corrigidos: descrição da tag no main.py ainda dizia "sempre 0"; o teste de by-item agora pede a página 2 com per_page=1; OPERATOR e AGENCY_MANAGER ganharam o 403 no pytest; o card mostra centavos, como o relatório; o acento do card passou para #00ACF0 (brand.info, dado neutro), da paleta; o e2e liga o perfil ECONOMICO em vez de sobrescrever a seleção personalizada guardada; o texto do vazio não manda "ativar" uma composição a quem já tem uma.
  • Minor, sem mudança: ICON.creditFinance é usado pelo PageHeader, e o menu segue com a string literal — é como os demais itens de nav-groups.tsx estão escritos.

Fase 3, rodada 5 — achados e correção:

  • Important (A1): a tela mostrava uma conciliação que não existe — "Custo real R$ 0,00", "Diferença −R$ X" igual ao estimado inteiro, e "Nenhuma divergência no período" sem extrato nenhum. Corrigido nas duas pontas: a API devolve actual_total null quando nada foi conciliado (RN17a, contrato na seção 12), e a tela mostra "—", não calcula diferença sem valor real e, sem extrato, explica que não há com o que conciliar em vez de afirmar "nenhuma divergência". Testes novos de API e de tela, com red-green.
  • Important (A2/CA11): o ramo mobile não tinha teste e tinha três defeitos — campo de busca que não aceitava texto nem filtrava, cartão por item só com o estimado, e o vazio sem link para Parâmetros. Corrigido: busca opcional no MobileFilterBar, cartão com estimado/real/diferença, link no vazio; três casos de tela dublando matchMedia e um e2e em 375px.
  • Important (T1): a variação da RN21 e a projeção do período aberto não tinham teste que falhasse se a regra mudasse. Corrigido com testes de data fixa (monkeypatch sobre hoje_local) — e a variação mudou de regra no caminho (decisão do usuário, Registro).
  • Important (Q/e2e): o finally do e2e restaurava só o perfil, e o cabeçalho dizia que desfazia o token. Corrigido: restaura o perfil e, se o original era PERSONALIZADO, a seleção; o cabeçalho diz que o token não volta e por quê (a API não expõe nem apaga o token).
  • Important (T2): o isolamento de tenant só era testado no summary. Corrigido: by-item no mesmo teste; o 403 do ADMIN_GLOBAL passou a cobrir os três endpoints.
  • Minor, corrigidos: teste de nome enganoso (..._sempre_zero_mesmo_com_valor_gravado) substituído; teste do clique no card sem asserção agora confere o destino; descrição do Swagger da RN21 alinhada ao código (o card soma o efetivo, linha a linha); _FUSO_LOCAL duplicado trocado pelo FUSO_LOCAL público; período invertido agora é 422 CREDIT_FINANCE_INVALID_PERIOD; o teste de período fechado deixou de depender da hora UTC; CA16 do e2e testa AGENCY_USER e AGENCY_MANAGER com o título certo; o FilterPill/FilterSelect de período não oferece mais "Todos" (allLabel={null}), e o desktop usa FilterBar/FilterSelect; os números do resumo reusam o SummaryCard; variação no formato pt-BR; o card não é clicável sem credit_finance:read; o vazio não oferece Parâmetros ao VIEWER; o erro de carga ganhou "Tentar de novo"; a tabela por item pagina (7.1.C); o card ganhou o chip de quantidade (7.1.D), com current_month_count no contrato.
  • Minor, sem mudança: month_over_month sem guarda de RN24 — o ADMIN_GLOBAL chega com tenant_id None, que o filtro incondicional do repositório transforma em zero linhas; documentado na docstring.
  • [SPEC INCOMPLETA] levado ao usuário e decidido: o que entra no gasto e na quantidade (RN17a) e contra o quê a variação compara (RN21). Ver Registro.

Fase 2, rodada 3 — achados e correção:

  • Critical (T2): faltava teste de 403 para papel sem credit_checks:rerun nos endpoints rerun/add (VIEWER, CA18; AGENCY_USER). Corrigido: test_viewer_recebe_403_em_rerun_e_add, test_agency_user_recebe_403_em_rerun.
  • Critical (T2): faltava teste de 404 para análise de outro tenant nos três endpoints novos (CA19). Corrigido: test_endpoints_novos_recusam_analise_de_outro_ tenant_com_404, cobrindo rerun/add/raw.
  • Important (A2/RN11): o card SKIPPED não trazia o link para Parâmetros que RN11/ CA8 exigem. Corrigido em CreditCheckGroup.tsx — link condicional ao motivo ser falta de token (não ao RN1a, que não tem o que configurar); dois testes novos.
  • Important (T1): faltavam testes para CREDIT_CHECK_ALREADY_RUNNING, CREDIT_CHECK_MUTUALLY_EXCLUSIVE (via add) e CREDIT_CHECK_TOKEN_NOT_CONFIGURED. Corrigido com três testes novos (test_rerun_item_ainda_em_andamento_e_recusado, test_add_mutuamente_exclusivo_e_recusado, test_rerun_e_add_sem_token_configurado_ sao_recusados).
  • Minor (Q4): descrição de credit_checks:read no rbac_seed.py citava renters:reanalyze_credit_bureau — quem de fato substitui essa ação é credit_checks:rerun. Corrigido.
  • Minor (Q2): credit_bureau_sandbox/README.md ainda referenciava credit_bureau_service.py/test_credit_bureau_service.py, removidos nesta fase. Atualizado para api_full_client.py/test_api_full_client.py.
  • Minor (Q1): show.tsx calculava canRerunCreditCheck/canReadRawCreditCheck pelo atalho can(), que concede tudo a ADMIN_GLOBAL — inconsistente em princípio com RN24 (inofensivo hoje porque credit_checks já vem null para esse papel). Corrigido para checar a permissão direto, sem o atalho.
  • Minor (Q4): linha de import em credit_analyses.py acima de 120 colunas. Quebrada em múltiplas linhas.

Fase 2, rodada 4 — achado e correção:

  • Important (Q3): CreditAnalysisService.search() chamava build_response_fields dentro do loop de montagem da página — uma consulta a credit_checks por análise listada, em vez do padrão de carga em lote que o mesmo método já usa para properties_map/creators_map/approvers_map/guarantees_map. Corrigido: CreditCheckRepository.latest_per_item_bulk() (uma consulta para todas as análises da página) e CreditCheckService.build_response_fields_bulk(), chamado uma vez por página em search(). Teste novo, test_listagem_traz_credit_checks_de_cada_analise_sem_misturar, prova que o agrupamento por análise não mistura o item de uma análise com o de outra.

Defeitos aceitos (Fase 1): nenhum. As duas recomendações da rodada 2 (o rename de credit_bureau_token→credit_check_token, RN6, e a heurística de podeVerPreco olhar só o primeiro item do catálogo) não bloquearam a aprovação — a primeira já está registrada como divergência deliberada, adiada para a Fase 2 (seção 18); a segunda é observação sem efeito hoje, revisitada se o mascaramento deixar de ser tudo-ou-nada.

Fase 1, rodada 1 — achados e correção:

  • Critical (A1): item is_active=false era aceito na seleção personalizada (RN1a/ 10.3 exigem 404). Corrigido: validate_codes_exist agora filtra por is_active; teste novo test_item_inativo_e_404_na_selecao_personalizada.
  • Important (A1): faltava a exigência de token para credit_check_profile != NONE (seção 12, 422 CREDIT_CHECK_PROFILE_REQUIRES_TOKEN). Adicionada em tenant_settings_service.py, mesma regra da RN2 da 0050; teste novo test_profile_diferente_de_none_sem_token_e_recusado.
  • Important (A2): código de erro do item do catálogo saía ITEM_DO_CATÁLOGO_NOT_FOUND (acentuado, fora do padrão) em vez de CREDIT_CATALOG_ITEM_NOT_FOUND como a spec documenta. Corrigido com um erro dedicado (_catalog_item_not_found), código literal.
  • Important (A1): credit_check_composition_total_estimate estava documentado na seção 12 mas não existia no schema nem era calculado. Adicionado a TenantSettingsResponse e computado em _to_response, mascarado (null) para AGENCY_USER/AGENCY_MANAGER como o restante do RN25.
  • Important (T1/T3): a contagem de testes desta seção estava errada ("16" web, "6" e2e) — corrigida para os números reais (8 web, 5 e2e) na seção 14.
  • Minor (A1): RN25/10.1/12 diziam que os campos de preço "saem do JSON" — na implementação eles vêm null (chave presente). Texto da spec corrigido nas três seções para descrever o comportamento real.
  • Minor (T1): RN3 (perfis prontos) não tinha teste de API exercitando CreditProfileItemRepository/resolve_active_codes com linhas reais. Adicionado test_perfil_pronto_resolve_via_credit_profile_item.

Suíte cheia revalidada depois das correções: 907 verdes, 26 vermelhos — os mesmos já registrados como falha de ambiente (SMTP/Mailpit inalcançável deste container, RN nenhuma desta spec) mais o intermitente documentado no CLAUDE.md (que desta vez passou isolado). make test-web e o e2e de Fase 1 não precisaram de nova rodada — as correções foram só de apps/api.

Defeitos aceitos: o que a revisão apontou e se decidiu não corrigir nesta spec, com quem decidiu e por quê. Vazio se não houver.

18. Registro

Depois do fechamento

  • O preço passou a se acertar sozinho pelo extrato (RN26, 17/09). O usuário pediu que o preço fosse atualizado uma vez por dia "a partir da pesquisa da API Full". Não há pesquisa de preço: o contrato público não expõe tabela nenhuma — o que existe é o extrato, com o valor cobrado de cada consulta na conta da garantidora. Daí as duas decisões dele: o acerto vale para o preço da própria garantidora (e não para o default do catálogo, que espalharia o preço de uma conta para as outras), e só onde ninguém digitou preço; no item com preço digitado, a tela continua só sugerindo (RN18).

  • A configuração saiu de Parâmetros para o menu (17/09). Pedido do usuário: quem precisa ligar as consultas não achava onde, porque a composição, o token e os preços viviam num bloco no meio de Parâmetros. Virou tela própria — Análise de Créditos, em ADMINISTRAÇÃO, rota /settings/credit-checks, mesmo porteiro de Parâmetros (ADMIN_TENANT) — e Parâmetros ficou com um aviso apontando para lá. O relatório financeiro segue em OPERAÇÃO com o nome de sempre (decisão do usuário entre três arranjos: item novo com nome próprio, dois itens homônimos, ou abas dentro do relatório). A API não mudou: a tela lê e grava o mesmo PATCH /settings.

  • A tela de configuração chama-se "Análise de Créditos" (decisão do usuário, 17/09). O item de menu e o título usam esse nome; o resto do produto mantém "consulta de crédito", que é o que cada chamada paga é — o relatório de gastos, os cartões dentro da análise e a tela do inquilino não mudam. As telas que citam o destino ("configure o token em…") passaram a usar o nome novo. O módulo Análise de Crédito (OPERAÇÃO) segue o que sempre foi: onde a análise acontece; a semelhança de nome foi levada ao usuário antes de aplicar.
  • O rodapé da tela nova quebrava em 375px. O botão de salvar era deslocado pela altura da tab bar, e a casca do celular já termina acima dela: sobrava uma faixa por onde o conteúdo aparecia por baixo do rodapé. Passou a usar a mesma forma de Parâmetros (bottom: 0). Junto, o subtítulo encurtou para caber sem reticências e o painel de baixo deixou de repetir o título da página ("Composição e preços").
  • A tela passou a explicar como conectar a API Full, também a pedido do usuário: o que é o serviço, que o cadastro é grátis e pré-pago, onde adicionar saldo e de onde copiar o Bearer Token, com os links conferidos em 17/09 — app.apifull.com.br/cadastrar, app.apifull.com.br e o guia de autenticação em doc.apifull.com.br/guides#authentication. Os links que apontavam para Parâmetros (o cartão SKIPPED sem token, a sugestão de preço da RN18 e os avisos do relatório) passaram a apontar para a tela nova.

  • Os quatro itens restantes entraram (16/09), com uma chamada real cada. Decisão do usuário depois de liberar o primeiro: autorizar as quatro de uma vez (R$ 19,72 no total, saldo da conta em R$ 5,77 depois). O que cada formato ensinou, e que a documentação não dizia: o CADIN devolve um resumo com quantidade e valor consolidado, e não só um "tem/não tem"; o Protesto Nacional traz o sintético com quantidade e valor, e zero protestos é informação — distinguir isso da ausência do bloco é o que separa SUCCESS de FAILED; o SCR separa o consolidado em quatro blocos (a vencer, vencido, limite e prejuízo), e somar o limite à dívida inflaria a exposição, então ele vai num campo próprio; e a QUOD traz o serviço de protestos dentro da consulta, que pode voltar indisponível (foi o caso) — aí os campos de protesto não entram, em vez de virarem zero, e quem decide isso é o status do subserviço (erro/situacao), não o número que ele manda junto.

  • A revisão desta leva (aprovada, 8) pediu três acertos de manutenção, feitos: o Swagger dos endpoints da RN9 lista o 422 CREDIT_CHECK_CEP_REQUIRED; as docstrings do client diziam que cinco métodos levantavam NotImplementedError (viram documentação no /wiki); e dados_financeiros tinha cópia própria do caminho comum de chamada, que já divergira no timeout — passou a usar _consultar, como os outros quatro. Dos Minor: o prejuízo do SCR passou a contar como restrição no selo do cartão, o CEP do inquilino ganhou validação de formato (um CEP pela metade dizia "não cadastrado"), o worker busca a análise no escopo do tenant, e o e2e do catálogo deixou de ter uma asserção que não podia falhar.
  • RN10 antes da RN11. A checagem do CEP vinha antes do reaproveitamento: com resultado pago e dentro da janela, a análise nova nascia SKIPPED pedindo um cadastro que não mudaria nada. O reaproveitamento passou a vir primeiro.

  • DADOS_FINANCEIROS foi implementado e ativado (16/09). Sem exemplos de resposta na plataforma da API Full, o usuário autorizou uma chamada real, com o CPF e o CEP dele, a /api/pf-dados-financeiros. O retorno é enxuto e todo em faixas de texto: dados.dados_financeiros com renda_estimada_v2, patrimonio_estimado, classe_social e poder_de_compra. A cobrança apareceu no extrato como "Dados Financeiros", R$ 1,28 — o mesmo preço que o catálogo já estimava, e o statement_label que a conciliação (RN16) precisa. Nada de dado real foi versionado: o resultado do sandbox é fictício, no formato real, e a resposta da chamada ficou fora do repositório.

  • A consulta exige CEP, e o inquilino PF não tinha nenhum. A RN6 da spec 0004 zerava o endereço inteiro do PF, e nenhum dos 124 inquilinos do dev-remote tinha CEP; só 8 dos 106 imóveis tinham. Decisão do usuário: o cadastro de PF passa a ter só o campo de CEP, opcional, com o CEP do imóvel da análise como reserva; sem os dois, a consulta nasce SKIPPED com MISSING_DATA e a tela aponta para o cadastro do inquilino. As alternativas recusadas eram usar só o CEP do imóvel (quase sempre vazio hoje) e o CEP da garantidora (avaliaria todo mundo pela região do escritório).

  • Liberar os cinco itens desligados entra nesta branch, antes do merge (decisão do usuário, 15/09). Levantada a documentação pública da API Full (doc.apifull.com.br/openapi.json): as cinco consultas têm rota e envio confirmados — CADIN → /api/cadin, DADOS_FINANCEIROS → /api/pf-dados-financeiros, PROTESTO_NACIONAL → /api/protesto-nacional, SCR_PREMIUM → /api/scr-premium, e QUOD_PF ambíguo entre /api/pf-quod-consulta, pf-quod-score, pf-quod-negativo-* e ic-quod —, mas a resposta não é documentada: todas devolvem o envelope genérico (dados, "os campos disponíveis dependem da base consultada"), sem campos nem exemplo. Sem o formato não há como escrever a tradução para normalized (RN7), nem o resultado fictício do sandbox. O usuário escolheu pedir exemplos de resposta ao suporte da API Full, sem custo, em vez de uma chamada paga por item; a spec volta a EM IMPLEMENTAÇÃO e o merge espera esses exemplos. Pedido também, para cada consulta, o nome exato que aparece no extrato de créditos — é o statement_label com que a conciliação (RN16) casa a cobrança.

  • Perfil pronto escolhido antes de salvar mostrava lista vazia e total R$ 0,00. Relatado pelo usuário no celular, com o sandbox ligado — mas o sandbox não era a causa. Com a composição salva em "Nenhuma", escolher "Econômico" no seletor não mostrava os itens do pacote: a tela montava a lista a partir de selected, que só descreve a composição salva, e a API não dizia quais itens compõem cada perfil pronto (RN3 é dado de plataforma). Cada item do catálogo em GET /settings passou a trazer profiles, e a tela usa isso nos perfis prontos. Teste de tela reproduzindo o caso (vermelho sem a correção) e teste de API do campo novo. Na conferência em 375px apareceu mais um: a descrição de cada item era cortada no meio da palavra (noWrap num texto inline, sem reticências); agora quebra a linha.

Revisão da branch

  • O glossário volta, por cartão. A remoção do "O que significam estes números?" não estava registrada; perguntado, o usuário quis de volta. Cada cartão mostra só os verbetes dos campos que tem — o conteúdo de hoje é o do Boa Vista.
  • O job só entra na fila depois do commit. Defeito herdado da 0050; o usuário escolheu corrigir agora a registrar como pendência. O mecanismo (depois_do_commit, na CommitRoute) é genérico: serve a qualquer aviso a outro processo sobre o que a requisição gravou.
  • Troca do principal depois da submissão: as consultas ficam, com o nome. A spec dizia de onde vem o renter_id (o principal), mas não o que acontece com as consultas quando o principal muda em PENDING/IN_ANALYSIS. Perguntado, o usuário escolheu manter os cartões como histórico, cada um dizendo de quem é, e fazer "Reexecutar" e "Adicionar" consultarem o principal de agora. Recusadas: disparar a composição de novo sozinho na troca (custo sem clique) e esconder as consultas antigas.
  • O total estimado de Parâmetros soma só o que roda. O CA1 pedia "o total somado" sem dizer se o item "Ainda não disponível" entra; somado, o Econômico mostrava R$ 5,31 por análise que custa R$ 3,23. Decisão do usuário: só o disponível.
  • RN2 — o item que só ficou SKIPPED não conta para a exclusão mútua. Decisão do usuário: no perfil Completo, o QUOD indisponível impedia adicionar o Boa Vista, e nenhuma consulta de score rodava nem podia ser adicionada.
  • Consulta parada (STALE_AFTER, 5 minutos) — parâmetro técnico, não decisão. Mesma natureza dos limiares da RN20: o job tem 30 segundos, e o número só separa "ainda vai terminar" de "não termina mais".
  • O campo do token saiu de dentro da composição. A Parte II o punha junto dos itens, e com "Nenhuma" ele sumia — mas a consulta avulsa da RN9, que vale com qualquer composição, precisa dele, e o cartão SKIPPED aponta para lá.
  • RN10 — a consulta em andamento: espera e reaproveita. A RN10 dizia que "nem PENDING em andamento é duplicado" sem dizer o que a segunda análise recebe, e o código só enxergava SUCCESS — duas análises do mesmo locatário submetidas juntas pagavam duas vezes. Perguntado, o usuário escolheu esperar: a nova nasce PENDING, apontando para a original, sem entrar na fila, e o worker a fecha quando a original termina. A alternativa recusada era recusar a segunda com erro, o que deixaria a análise sem aquela consulta.
  • RN5 — o histórico de preços entrou agora. Nenhuma fase o implementou (o método de repositório existia, sem endpoint nem tela), e não havia registro de adiamento. O usuário escolheu implementar antes do merge, e não adiar.
  • O painel do inquilino volta, no formato novo. A Fase 2 tirou da tela do inquilino o painel de bureau que a 0050 tinha posto ali a pedido do usuário, sem registrar. Ao ser perguntado, o usuário quis as consultas de volta no inquilino: a tela mostra as consultas dele em todas as análises, cada uma com o número da análise, só leitura — reexecutar continua sendo na análise, onde está o custo e o status que a RN9 exige. GET /renters/{renter_id}/credit-checks, sob credit_checks:read.
  • RN9 — lista de permissão, e um código de erro para o rascunho. O código recusava só os status decididos. A RN9 sempre foi lista de permissão, então não é decisão nova; o código CREDIT_CHECK_ANALYSIS_NOT_SUBMITTED é, porque ..._DECIDED descreveria mal um rascunho para quem integra.
  • RN24 × seção 11 — a guarda de código vale para os endpoints autônomos também. A seção 11 dispensava a guarda do ADMIN_GLOBAL nos endpoints de credit_checks:* porque a permissão já barra; a RN24 dizia o contrário. Resolvido a favor da RN24, que é a regra mais restritiva e a única que a fixture de teste consegue provar.
  • skip_reason — o motivo do SKIPPED passa a ser dado. Coluna nova em credit_checks, na migration da Fase 4 (que ainda não foi para a produção, então não pede migration própria).
  • O catálogo não ativa item sem endpoint. PATCH /credit-catalog/{code} com is_active: true num item de endpoint nulo é 422. Antes, o ADMIN_GLOBAL conseguia deixar um item que a submissão marcava SKIPPED e a consulta avulsa deixava FAILED.

Fase 4 — Sincronização do extrato e conciliação

  • A condição do plano se cumpriu no começo da fase, por fora da spec. A spec registrou em 09/09 que o extrato da API Full não tinha contrato público. Em 13/09 a documentação passou a publicar um OpenAPI (doc.apifull.com.br/openapi.json) com GET /api/credits/statement e GET /api/get-balance. A resposta vem num envelope genérico, então o formato das linhas foi conferido numa chamada real, autorizada pelo usuário, com o token já salvo da garantidora "Garantidora" no dev-remote — duas chamadas que não consomem crédito (saldo e extrato), nada gravado, token nunca exibido. O fixture versionado (credit_bureau_sandbox/apifull_credits_statement.json) repete o formato com valores fictícios: o arquivo derivado da resposta real foi recusado pelo ambiente ao ser gravado no repositório, e com razão — ids de transação, datas e saldo são da conta.
  • O extrato não traz CPF nem identificador da requisição — a RN16 previa casar por provider_request_id quando houvesse. Não há. O casamento ficou por item (o nome da consulta no extrato, guardado no catálogo em statement_label, coluna nova) e por tempo (15 minutos entre a conclusão da consulta e o lançamento). Com duas consultas do mesmo item a poucos minutos uma da outra, cada lançamento casa com a mais próxima, e uma consulta nunca casa duas vezes.
  • RN19 mudou de chamada. A regra previa o extrato de um dia; o contrato mostrou o saldo da conta, que não consome crédito e não depende do formato do extrato.
  • Três colunas que a spec não listava: credit_check_last_sync_at (o relatório precisava de onde ler a "última sincronização"), raw_response_purged_at (a RN13 a nomeava, mas a seção 11 não) e statement_label. Entraram na migration c4f1a9d2e7b3, com server_default nos timestamps — a lição da Fase 2.
  • O horário do lançamento é gravado em UTC. O provedor manda horário de São Paulo sem fuso; e o SQLite da suíte descarta o fuso de DateTime(timezone=True) e compara texto. Só a mesma base dos dois lados deixa o casamento certo nos dois bancos.
  • O disjuntor (RN20) falha aberto. Com o Redis fora do ar, ele se comporta como fechado: é uma proteção, e não pode virar o motivo de nenhuma consulta rodar. A suíte troca o armazenamento por um em memória (fixture autouse no conftest.py) — nenhum teste depende de Redis.
  • FAILED cobrado vira divergência, não custo. Se a API Full cobrar uma consulta que falhou, o lançamento não casa (só SUCCESS casa, como a RN17 descreve) e aparece como "lançamento sem consulta" — é o sinal certo para quem confere, e mantém a RN17a (o que conta no gasto) intacta.
  • Sandbox. No dev-remote, "Sincronizar agora" monta o extrato a partir das próprias consultas do tenant, cobradas pelo preço estimado, e passa pelo mesmo parser — é o que o e2e exercita. Por isso a sugestão de preço (RN18) nunca aparece no sandbox: as cobranças sempre batem com o estimado. Ela é coberta pelo pytest.
  • Os cinco produtos inativos ficam inativos (RN1a, decisão do usuário): o contrato confirma a requisição deles, mas a resposta pede uma chamada real paga a cada um.

Fase 3 — Relatório financeiro e card

  • Duas decisões de negócio que a spec não tinha tomado, levadas ao usuário pela revisão da fase (rodada 5) e respondidas. O que conta no gasto e na quantidade: só SUCCESS, PENDING e RUNNING — FAILED e SKIPPED ficam fora (virou a RN17a). A primeira versão contava tudo, e a tela mostrava "CADIN — 16 no período" para dezesseis consultas SKIPPED que nunca rodaram. Contra o quê a variação do card compara: os mesmos dias do mês anterior, do dia 1 até o mesmo dia (RN21 reescrita). Contra o mês anterior inteiro, todo começo de mês mostraria uma queda que não aconteceu.
  • "Não conciliado" deixou de ser zero (RN17a, contrato da seção 12): actual_total vem null sem conciliação. O coalesce(..., 0) da primeira versão fazia a tela dizer "custo real R$ 0,00" e calcular uma diferença do tamanho do gasto inteiro.
  • 7.3 pedia, no vazio de "ainda não sincronizou", um link para "clicar em Sincronizar agora" — que não pode existir nesta fase: o botão fica desabilitado até a Fase 4. A tela explica que não há extrato e que a sincronização ainda não está disponível; o link para Parâmetros aparece no vazio do gasto por consulta, e só para quem abre Parâmetros (ADMIN_TENANT). O VIEWER lê o relatório, e o link o levaria ao acesso negado.
  • 7.2.D supunha que a grade do Centro de Operações quebra linha em telas estreitas — ela não quebrava. Com flex: 1 e minWidth: 0, o SummaryCard espremia todos os cards numa linha só em 375px, com título e chips cortados; o card desta spec, o quinto, piorava. Corrigido no SummaryCard (flex-basis de 160px: dois por linha no celular, todos numa linha no desktop). Muda o Painel inteiro no mobile, não só o card novo.
  • Componentes compartilhados estendidos, de forma aditiva: MobileFilterBar com busca opcional (a tela de relatório não tem o que buscar, e o campo aparecia sempre) e allLabel={null} no FilterPill/FilterSelect para o filtro de período, que sempre tem valor. Quem não passa nada continua como estava; os .prompt.md dos dois dizem isso.
  • O card do Centro de Operações não é clicável para quem não tem credit_finance:read. O card é de operations_center:read (RN21) e o relatório é de credit_finance:read; o ANALYST vê o card, e o clique o levaria ao acesso negado.

  • O plano dizia "os quatro endpoints de /credit-finance" e só nomeava três — summary, by-item, divergences. O quarto (POST /credit-finance/sync) pertence à Fase 4 pelo próprio texto do plano ali (ProviderStatementEntry/StatementSyncService /POST .../sync estão listados sob "Fase 4", não "Fase 3") — a contagem "quatro" contradizia a própria lista da frase seguinte. Corrigido para "três" antes de implementar; achado de contagem, mesmo tipo já resolvido tecnicamente nas fases anteriores (RN7, CA4), não decisão de negócio.

Fase 2 — Execução e tela de análise

  • A migration 937f3e75d39d criava credit_checks.created_at/updated_at sem server_default, ao contrário do que TimestampMixin já faz no model (server_default=func.now()). A migration escrita à mão (não por autogenerate) não herda esse detalhe do model sozinha — o resultado era todo INSERT (via CreditCheckRepository.create_isolated, que não passa created_at explicitamente, confiando no default) falhando com NotNullViolationError, silenciosamente engolido pelo próprio try/except de create_isolated (RN8a: uma falha de gravação não pode derrubar a submissão) — a análise submetia normalmente, só que com credit_checks: [] sempre, em qualquer tenant. Só apareceu rodando o e2e de verdade contra o dev-remote — a suíte de pytest (SQLite) não pegou porque lá o teste usa Base.metadata.create_ all(), que gera o DDL a partir do model (com o server_default correto), nunca da migration escrita à mão; é exatamente o tipo de divergência que só a Fase 4 do /fix//spec ("rodar contra o ambiente real") existe para pegar. Corrigido na própria migration (server_default=sa.text("now()") nas duas colunas), com downgrade/ upgrade reexecutados contra o Postgres real do dev-remote para provar a correção — não só reescrito e presumido certo.
  • RN9 ("adicionar consulta avulsa a qualquer análise submetida") e 7.3 ("tenant com composição 'Nenhuma' — o grupo de cartões não aparece, bloco inteiro ausente") entraram em tensão, descoberta rodando o e2e contra o dev-remote — resolvida pelo usuário. CreditCheckGroup implementava 7.3 à risca: com credit_checks: [] (perfil NONE, ou nenhum item elegível), o bloco inteiro sumia — cartões e o botão "Adicionar consulta" junto, porque os dois viviam no mesmo componente. O efeito era RN9 inalcançável exatamente quando o tenant nunca ativou composição nenhuma. Decisão do usuário: o botão "Adicionar consulta"/"Reexecutar" passa a depender do status da análise, não da lista de cartões estar vazia — continua disponível em SUBMITTED/ IN_ANALYSIS/PENDING mesmo sem nenhum CreditCheck ainda, e some só quando a análise já foi decidida (APPROVED/REJECTED/CANCELLED, RN9/CA6a, novo) ou quando não há mesmo o que fazer (sem credit_checks:rerun). RN9, 7.3, CA6a e o contrato de rerun/add (seção 12, 422 CREDIT_CHECK_ANALYSIS_DECIDED) foram atualizados juntos. O e2e de "adicionar avulso a partir de perfil NONE", removido na primeira versão desta fase por não ter onde clicar, volta a existir e a passar — a mesma composição NONE que antes escondia o bloco inteiro agora só esconde os cartões, não o botão.
  • credit_finance:read/credit_finance:sync não entram no seed nesta fase, ao contrário do que a tabela de 10.2 lista (que descreve o estado final, depois das quatro fases). Não há endpoint /credit-finance/* ainda (Fase 3/4) — declarar as duas agora seria exatamente o que a nota de pendency_types:read em rbac_seed.py já adverte: permissão sem endpoint que a exija responde "sim" a isto está protegido? sem proteger. Descoberto pelo próprio meta-teste do repositório (test_authorization_coverage.py::test_toda_permissao_definida_e_exigida), que reprovou com as duas como órfãs. credit_checks:read/rerun/read_raw entram normalmente — têm endpoint real nesta fase. A Fase 3 soma as duas junto do endpoint, na mesma tabela.
  • O mesmo meta-teste também apontou credit_checks:read como órfã, por um motivo diferente: RN22 manda checar essa permissão dentro do service, sem recusar a requisição (o campo só some, GET /credit-analyses/{id} continua 200) — nenhum dos dois padrões que o meta-teste já reconhecia (require_permission do router, exigir_permissao do serviço, que sempre lança) descreve isso. Adicionado um terceiro padrão ao meta-teste (current_user.has_permission("recurso", "ação"), o primitivo que os outros dois já usam por baixo) — mudança na régua de verificação, não no código sob verificação, e com o mesmo raciocínio que já justifica os outros dois formatos.
  • RN7 corrigida antes da implementação do ApiFullClient. A tabela original mapeava BOAVISTA_ESSENCIAL para uma lista abstrata (score, score_range, has_negative_records, negative_records_count, negative_records_total_amount, has_protests, protests_count, protests_total_amount) e afirmava que eram "os mesmos campos do parser já existente na 0050" — não são: _extrair() (credit_bureau_service.py:55-66,109-147) produz score, classification, default_probability_percent, financial_pendencies_count, financial_pendencies_total, protests_count, bounced_checks_count, presumed_income_range. Dois campos reais (bounced_checks_count, presumed_income_range) se perderiam sob a nomenclatura abstrata, e duas colunas da tabela (has_negative_records, score_range) não correspondem a nada que o parser produza. Corrigido na RN7 (seção 9) e no exemplo de GET /credit-analyses/{id} (seção 12) para os nomes reais, antes de escrever ApiFullClient.boavista_essencial() — divergência técnica descoberta relendo o parser já confirmado, não decisão de negócio; a linha de QUOD_PF continua abstrata de propósito (RN1a, endpoint ainda não confirmado).
  • CA4 presumia os três itens do perfil ECONOMICO com endpoint confirmado — não é o caso hoje: a migration eb1df26c8862 (Fase 1) semeou só BOAVISTA_ESSENCIAL com is_active=true/endpoint preenchido; DADOS_FINANCEIROS e CADIN nascem is_active=false/endpoint=null (RN1a). Implementar o disparo real (RN8a/RN11) contra o seed de verdade, em vez de contra a leitura solta do critério, mostrou a divergência. Corrigido o texto de CA4 (seção 13) para descrever o estado real (um cartão evolui, dois nascem SKIPPED) — comportamento, não achado de bug: é exatamente o que RN11 manda fazer quando o endpoint não está confirmado.
  • RN20 (rate limit/circuit breaker) não aparece em nenhuma fase do plano nem em nenhum CA — achado ao ler a seção 14 por completo antes de começar a Fase 2. Não bloqueia esta fase (nenhum critério de aceite depende dela) nem é regra de segurança/isolamento (é proteção operacional contra consumo em loop) — fica registrado aqui como pendência de planejamento, não implementada nesta fase; entra explicitamente na Fase 4 (que já mexe no mesmo ApiFullClient/worker) ou pede confirmação do usuário se ele preferir outro momento.

Fase 1 — Catálogo, configuração, token e estimativa

  • credit_bureau_type/credit_bureau_token não foram removidos nesta fase, ao contrário do que a seção 11 e o plano diziam. Motivo descoberto ao implementar: esses dois campos ainda sustentam o disparo automático da RN4b da 0050 (credit_analysis_service.py, credit_bureau_worker_service.py, credit_bureau_schedule.py) — removê-los antes de a Fase 2 trocar esse disparo pela versão baseada em CreditCheck (RN8a) deixaria o agendamento hoje em produção quebrado no meio das quatro fases. Os dois modelos convivem até a Fase 2 fazer o corte.
  • O catálogo global ficou em GET/PATCH /credit-catalog, sem o prefixo /admin que a seção 12 descrevia. Levantamento do código mostrou que nenhum outro recurso da API usa esse prefixo — tenants.py, por exemplo, é só /tenants, gated por permissão; a distinção "isto é do SaaS admin" vive no frontend (/app/admin/*), não no path da API. Seguir o prefixo inventado teria sido a única inconsistência do repositório.
  • A UI de Parâmetros ficou aditiva, não substituindo o Select binário existente — mesma razão da primeira divergência: o painel novo ("Consultas de crédito") convive com o antigo ("Análise de crédito") até a Fase 2, quando o corte de execução torna o antigo redundante e ele é removido.
  • Achado durante o TDD do apps/web: o primeiro teste do total recalculando ao marcar um item (Switch em modo personalizado) falhou — data-testid num <Switch> cru do MUI vai para um <span> interno, não para o <input>, e o clique do user-event não alcançava o onChange. Corrigido com slotProps={{ input: { "data-testid": … } }}, que também é o desenho mais correto para o e2e (o Playwright clica no elemento certo). Ficou registrado porque é o tipo de causa que não aparece lendo o código — só rodando o teste vermelho.
  • CurrencyTextField ganhou size e data-testid (antes não tinha nenhum dos dois) — extensão pontual do componente compartilhado para servir aos dois usos novos desta spec, sem quebrar o uso existente (initial_fee_value, que não passa nenhum dos dois e continua com o comportamento de antes).
  • Migration eb1df26c8862 verificada de duas formas: o --sql (modo offline) falha no op.bulk_insert de credit_catalog_items — limitação conhecida do SQLAlchemy/ Alembic, que não sabe renderizar JSON como literal fora de parâmetro vinculado. A aplicação real (alembic upgrade head, modo normal) não tem esse problema — confirmado rodando contra o Postgres do dev-remote de verdade: os seis itens e os dois perfis (ECONOMICO/COMPLETO) chegaram ao banco exatamente como a RN1/RN3 descrevem.

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:

  1. 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.
  2. 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.