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.
- A Parte I diz qual é o problema e como se convive com ele hoje. Revisada na seção 6 (necessidade × jornada de hoje). É o portão mais barato de todos, e o único que pergunta contra que problema isto está sendo construído.
- A Parte II diz como se usa o que vai existir. Revisada na seção 8 (jornada × viewports).
- A Parte III diz como se constrói. Revisada na seção 16 (segurança × clareza técnica) — autorização, isolamento, e se sobrou decisão para quem vai codar.
- A implementação é medida contra as três, na seção 17 (aderência × qualidade × testes) — uma revisão por fase, e uma da branch inteira antes de fechar.
Cada parte só se escreve depois que a anterior passou. Descrever tela sobre problema mal enquadrado, ou detalhar endpoint sobre experiência vaga, é o jeito mais caro de descobrir que era outra coisa.
Parte I — O problema¶
Aqui não entra solução: nem tela, nem endpoint, nem botão. Só o que dói, para quem, e o que se faz hoje. Se você já sabe qual é a tela, guarde para a Parte II.
1. Dor¶
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/desligacredit_bureau_typeem 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:
- O
ADMIN_TENANTabre Parâmetros e ativacredit_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. - A cada Análise de Crédito submetida (
DRAFT→SUBMITTED), o Garantora agenda automaticamente a mesma consulta (Boa Vista Essencial Positivo, endpointscpc-boavistada 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. - O resultado aparece no painel de bureau da análise (
BureauInquiryPanel) para oANALYSTdecidir — mas não há, ao lado dele, nenhuma informação de quanto essa consulta custou. - Se o
ADMIN_TENANTquiser 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. - 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.
- Se o
ANALYSTavaliar 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 doANALYST— 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
FAILEDde 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_TENANTjá 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_TENANThoje; o card novo entra na mesma grade dos existentes (SummaryCard), sem navegação extra para chegar até ele. - Jornada, caso feliz:
- O
ADMIN_TENANTabre Parâmetros → "Análise de crédito" e, em vez doSelectbiná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
ANALYSTabre 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_TENANTabre "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_TENANTjá 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
CreditCheckainda 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 — semcredit_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
Selectde 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
PENDINGmostra 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
FAILEDmostra 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). Semtenant_id, mantido peloADMIN_GLOBAL(mesmo padrão de dado de plataforma semTenantMixinqueRole/Permissionjá usam hoje —DocumentTypenão serve de precedente aqui, ele temTenantMixine é escopado por tenant). Campos:code(único, ex.QUOD_PF),provider(enumCreditCheckProvider; sóAPIFULLnesta entrega, campo existe para provedor futuro — fora de escopo, seção 5),endpoint(nullable — ver RN1a),name,description,person_type(enum; sóPFselecionável nesta entrega,PJreservado — 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 deBOAVISTA_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. endpointficanullpara os cinco, eApiFullClient(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 itensis_active=falseno seed, ativados um a um assim que o formato de cada um for confirmado — sem migration nova, só umUPDATEdeis_activee doendpoint.
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 —
TenantSettingstroca o campo binário por uma composição. O campocredit_bureau_type(NONE/BOAVISTA_CADASTRO_POSITIVO, RN1 da 0050) é removido. Entracredit_check_profile— enumNONE/ECONOMICO/COMPLETO/PERSONALIZADO, defaultNONE, mesmo padrão de enum-como-Stringjá usado emTenantSettings(native_enum=False). QuandoPERSONALIZADO, a seleção do tenant vive numa tabela novaTenantCreditCheckSelection(tenant_id,catalog_item_code) — uma linha por item ativo; trocar de perfil paraECONOMICO/COMPLETO/NONEnão apaga a seleção personalizada guardada, só deixa de ser a composição em uso (oPATCHgrava 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 removecredit_bureau_typesem backfill. - RN5 — Preço efetivo por tenant, com vigência (
TenantCreditCatalogPrice). Nunca sobrescrito in-place:PATCH /settingscom 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 fazUPDATE. O preço efetivo de um item, num instante, é ounit_priceda linha deeffective_frommais recente que não seja futura; sem nenhuma linha, é o default do catálogo (RN1) nocredit_check_price_leveldo tenant (1ou2, novo campo emTenantSettings, default1— nenhum tenant novo começa com preço negociado). OADMIN_TENANTvê o histórico de preços de um item (mesma tela de Parâmetros, lista simples poreffective_fromdesc, 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 paracredit_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, otoken_configured/token_last_4write-only e o modo sandbox (RN9b/RN9c da 0050) continuam idênticos — inclusive a variável de ambienteCREDIT_BUREAU_SANDBOX_MODEmantém o nome antigo de propósito: é infraestrutura já no.envdo 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. SubstituiCreditBureauInquiry/os camposcredit_bureau_*doRenter(RN6/RN9 da 0050):credit_bureau_status,credit_bureau_data,credit_bureau_requested_at,credit_bureau_queried_atsão removidos deRentersem backfill (mesma justificativa da RN4 — sem base instalada). Campos deCreditCheck:id,tenant_id,credit_analysis_id(FK),renter_id(FK — resolvido no service a partir doLESSEEprincipal da análise, nunca aceito do corpo, seção 10.3),catalog_item_code(FK),status(enumPENDING/RUNNING/SUCCESS/FAILED/SKIPPED),requested_by(FKUser— 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_atdesc), 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 doCommitRoute), o service resolve a composição ativa do tenant (RN4) e agenda um jobarqpor 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 seuCreditCheckemPENDINGantes de ser enfileirado (mesmoSAVEPOINTda 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 containerworker, mesma fila Redis já usada pela 0050. - RN8b —
ApiFullClient. Um client HTTP por provedor (sóAPIFULLnesta entrega), com timeout e captura de qualquer erro sem deixar subir exceção (mesma regra da RN7 da 0050 para o client existente) — fecha oCreditCheckcomoFAILEDcom 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 levantaNotImplementedErrorexplícito, nunca chamada em produção porque RN1a mantém esses itensis_active=falseno 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ãoFAILEDé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 workerarq(6h de São Paulo), ambos chamandoStatementSyncService, que paginaGET /api/credits/statementda 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 trazid,created_at(horário de São Paulo, sem fuso),direction(debit/credit),value,balance,consultation(o nome comercial da consulta) eobservation— sem CPF e sem identificador da requisição. O parser guarda a transação inteira emrawe é tolerante a campo ausente; o horário é gravado em UTC. Idempotência:unique(tenant_id, external_id)com oiddo 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 consultasSUCCESSdo 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 comCreditCatalogItem.statement_label(coluna nova, preenchida paraBOAVISTA_ESSENCIALcom "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 consultaSUCCESSnã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 preencheCreditCheck.unit_price_actualeProviderStatementEntry.matched_credit_check_id. - RN17 — Relatório de divergências. Lançamento sem
CreditCheckcorrespondente (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) eCreditCheckSUCCESSsem lançamento correspondente depois de um prazo razoável (7 dias corridos desdecompleted_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,PENDINGeRUNNING.FAILEDeSKIPPEDficam fora dos dois:SKIPPEDnunca 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) éSUCCESScom 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 consultaFAILEDnão entra, e o cartão dela diz "Não entra no custo". "Não conciliado" não é zero: o total real (actual_total) vemnullenquanto 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_totalsoma só as consultas conciliadas, então a diferença é contrareconciled_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_codede um tenant acumula 3 divergências consecutivas entreunit_price_estimatedeunit_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), comcreated_bynulo: é 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, deunit_price_actualquando conciliado, senãounit_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;nullquando 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 emGET /operations-center/overviewsob 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 queGET /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 campocredit_checks, checado dentro do service, não no router.GET /credit-analyses/{id}continua gated porcredit_analyses:read(permissão queADMIN_TENANT,ANALYST,OPERATOR,VIEWER,AGENCY_USEReAGENCY_MANAGERjá têm hoje, sem mudança) — mas o campocredit_checkssó é preenchido quandocurrent_usertambém temcredit_checks:read. Concedida aADMIN_TENANT/ANALYST/OPERATOR/VIEWER(10.2); não concedida aAGENCY_USER/AGENCY_MANAGERnem aADMIN_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) devolveNonesó olhandocurrent_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 deAGENCY_USER/AGENCY_MANAGER(canal, não paga: RN23) e o deADMIN_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_MANAGERnão têmcredit_checks:read, e é por isso que não veemcredit_checks. Mesmo resultado que a checagem porpartner_agency_idda 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 emGET /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_GLOBALtambém não temcredit_checks:read, mais uma guarda explícita no código contra a fixture de teste. Semcredit_checks:readna 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_GLOBALacabaria com a permissão e o campo apareceria — porqueGET /credit-analyses/{id}já é alcançável por ele hoje viacredit_analyses:read, independentemente desta spec (CreditAnalysisService._get_authorized,credit_analysis_service.py:1021-1035, usatenant_scope(current_user), que devolveNoneparais_admin_global,app/services/scoping.py:42-48— oBaseRepositoryentão não filtra por tenant nenhum, 10.3). Por isso as cinco permissões novas decredit_checks:*/credit_finance:*entram explicitamente nessa subtração (credit_catalog:read/updatenão entram — são as únicas duas queADMIN_GLOBALdeve ter, 10.5). E, além da subtração,CreditCheckService/CreditFinanceServicerecusam explicitamentecurrent_user.is_admin_globalem código — porque a fixture de teste doADMIN_GLOBALconcede 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
nullnoGET /settingspara quem não paga, sem depender de permissão nova. Diferente de RN22-RN24 (que são sobre o campocredit_checksde uma análise, ausente por permissão), esta é sobre a tela de configuração:AGENCY_USER/AGENCY_MANAGERmantêmtenant_settings:read(concessão de hoje, sem mudança) e continuam vendo quais consultas compõem o perfil ativo do tenant — só os camposunit_price/unit_price_source/credit_check_composition_total_estimate(seção 12) vêmnullna resposta para esses dois papéis, em vez do valor real. É mascaramento de valor na serialização — não some a chave do JSON, comocredit_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á regemcredit_bureau_type/credit_bureau_tokenhoje (ADMIN_TENANT,ANALYST,OPERATOR,VIEWER,AGENCY_USER,AGENCY_MANAGERleem; sóADMIN_TENANTescreve). Introduzir um recursocredit_confignovo, 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/CreditProfileItemnão têmtenant_id— são dado de plataforma (RN1/RN3), e é por isso que sóADMIN_GLOBALos administra; todo tenant lê o mesmo catálogo.TenantCreditCheckSelection,TenantCreditCatalogPrice,CreditCheckeProviderStatementEntrytêmtenant_id(TenantMixin) e são filtrados automaticamente peloBaseRepositorypara quem tem tenant — nenhuma query nova passatenant_id=Nonepara um chamador que não sejaADMIN_GLOBAL. Isso não cobreADMIN_GLOBAL:BaseRepository.get_by_idsó filtraif tenant_id and hasattr(...)(apps/api/app/repositories/base.py:51-56), eADMIN_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_idemCreditChecké resolvido no service, nunca aceito do corpo. O endpoint de reexecução/adição (RN9) recebe sócatalog_item_code; o locatário é sempre oLESSEEprincipal já vinculado àCreditAnalysisdo path — o mesmo padrão que a 0050 já usa para_bureau_inquiry_for.credit_analysis_iddo 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 decredit_checks(seção 12).catalog_item_codedo corpo é validado contra o catálogo ativo — vale para os caminhos onde a pessoa escolhe o código explicitamente:PATCH /settingscom perfilPERSONALIZADO(RN4) e os dois endpoints de reexecução/adição (RN9). Nesses caminhos, código inexistente ouis_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 viaCreditProfileItem(RN3): esses podem incluir itemis_active=false(RN1a — é o caso hoje deDADOS_FINANCEIROS/CADINem ambos os perfis), e o resultado é oCreditChecknascerSKIPPED(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_GLOBALe 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 porRole/Permission.ADMIN_GLOBALnão tem nenhuma das cinco permissões novas decredit_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), eADMIN_GLOBALnão tem filtro de tenant noBaseRepository(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}, queADMIN_GLOBALjá 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 decredit_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 colunaADMIN_GLOBALda 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), semTenantMixin.app/models/credit_profile_item.py—CreditProfileItem(RN3), semTenantMixin, 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), comCreditCheckStatus(enum).app/models/provider_statement_entry.py—ProviderStatementEntry(RN15).- Modelos alterados:
app/models/tenant_settings.py— removecredit_bureau_type; adicionacredit_check_profile,credit_check_price_level,credit_check_token(renomeado decredit_bureau_token),credit_check_reuse_enabled,credit_check_reuse_window_days,credit_check_token_validated_at(RN4, RN6, RN10, RN19).app/models/renter.py— removecredit_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; alteratenant_settings(drop de uma coluna, adição de cinco); alterarenters(drop de quatro colunas); populacredit_catalog_itemsecredit_profile_itemsviaop.bulk_insertno próprioupgrade()(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, devolvecredit_bureau_typeatenant_settings(defaultNONE) e as quatro colunas arenters(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, nuncaUPDATE) — mesmo arquivo que já validacredit_bureau_type != NONEexige token (RN2 da 0050), estendido para os perfis novos.CreditCheckService(novo) — RN8, RN8a, RN9, RN10, RN11, RN12: cria/lêCreditCheck, resolverenter_idno 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 levantamNotImplementedError(nunca alcançados enquantois_activeforfalse, 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 deGET /credit-finance/*(RN15, RN17) e alimentaapp/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.pye o jobdaily_credit_finance_jobemapp/worker.py. - Revisão da branch:
CreditCheck.skip_reason(enumCreditCheckSkipReason, na migration da Fase 4);CreditCheckService.list_for_rentere a rotaGET /renters/{id}/credit-checks;GET /settings/credit-check-prices/{code}/historyemtenant_settings.py; a espera da RN10 (CreditCheckRepository.in_progress_for_renter_itemewaiting_on, e o fechamento no worker);STALE_AFTEReCreditCheck.stalled(RN12); o passo depois do commit emapp/core/transaction.py(depois_do_commit), queCreditCheckServiceusa para agendar o job (RN8a). - RN26 (17/09):
CreditFinanceService.atualizar_precos_pelo_extrato(chamada pelodaily_credit_finance_job, por garantidora, logo depois da sincronização),TenantCreditCatalogPriceRepository.tem_preco_digitado, e a origemSTATEMENT_AUTOemCreditCatalogService.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_ruleguarda ocepdo PF; a migrationd5e2b7c1a9f4ativa o item; e o resultado de sandboxcredit_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 decredit_checks:*/credit_finance:*entram na subtração deADMIN_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/updatenão entra na subtração — são as únicas duas queADMIN_GLOBALdeve 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 campocredit_checkse as guardas de RN22-RN24. O método que monta esse campo paraGET /credit-analyses/{id}checacurrent_user.has_permission("credit_checks", "read")antes de preenchê-lo (RN22) — e, além disso, recusa explicitamentecurrent_user.is_admin_global(RN24), porque a fixture de teste doADMIN_GLOBALconcede 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 decredit_checks:*/credit_finance:*(rerun, adicionar, raw, histórico do inquilino, financeiro) usamrequire_permissione 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 orawprotegido 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 deBaseRepository(10.3): ele não filtra nada paraADMIN_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 emtenant_settings.py/credit_analyses.py) eresponse_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 emnormalized), matching de conciliação (RN16, com oprovider_request_ide sem ele), e os negativos de autorização de cada linha da tabela de 10.2 — inclusiveAGENCY_USER/AGENCY_MANAGERrecebendoGET /credit-analyses/{id}com o campocredit_checksnull(RN23, não é só "não vê a tela", é "o campo não traz dado nenhum no JSON") eADMIN_GLOBALrecebendo o mesmo camponullmesmo comcredit_analyses:readconcedido (RN24).
apps/web¶
- Páginas/componentes alterados:
pages/settings/index.tsx— a seção "Análise de crédito" (linhas 741-820 hoje) troca oSelectbiná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; viraCreditCheckCard(renomeado, um componente por item) mais umCreditCheckGroupque os agrupa e soma o custo (Parte II, 7.1.B). OtestIdPrefixexistente (credit- analysis-bureau) é reaproveitado como prefixo de cadaCreditCheckCard, com ocatalog_item_codecomo 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— umSummaryCarda mais (RN21), clicável só para quem temcredit_finance:read.pages/operations-center/components/SummaryCard.tsx—flex-basisde 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.tsxecomponents/shared/FilterBar.tsx— busca opcional noMobileFilterBareallLabel={null}noFilterPill/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 peloADMIN_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-testidnovos: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.tsxganha o recurso Refine (list: "/credit-finance") ecomponents/navigation/nav-groups.tsxganha o item emOPERAÇÃOcomrequerPermissao: "credit_finance:read"(Parte II, ponto de entrada C). - Menu e rota por permissão: quem não tem
credit_finance:readnã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_TENANTmonta 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_USERcontinua sem ver nenhum resultado de crédito, agora no formato novo (credit_checks: null, RN23) — mesma asserção que já existe parabureau_inquiry: null, só migrada para o campo novo. - Seeds (
tests/fixtures/seeds/): tenant com composiçãoPERSONALIZADOjá montada (para os specs de execução não depender da tela de configuração); umCreditCheckantigo (completed_athá 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)¶
{
"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)
{}
{ "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)¶
[
{
"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
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).
[
{
"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)
{}
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:
- Dropa as seis tabelas novas (
credit_catalog_items,credit_profile_items,tenant_credit_check_selections,tenant_credit_catalog_prices,credit_checks,provider_statement_entries). - Em
tenant_settings: devolvecredit_bureau_type(NONEpara todo mundo — não há como recuperar qual composição estava ativa, aceito porque não há base instalada, RN4); renomeiacredit_check_tokende volta paracredit_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. - 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_TENANTabre Parâmetros, escolhe o perfilECONOMICO, 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" nasceSKIPPEDe não custa (decisão do usuário na revisão da branch). - [x] CA2. No perfil
PERSONALIZADO, marcarQUOD_PFcomBOAVISTA_ESSENCIALjá marcado é recusado com422 CREDIT_CHECK_MUTUALLY_EXCLUSIVE(RN2), e a tela mostra o motivo sem perder a seleção já feita. - [x] CA2a.
PATCH /settingscom perfilPERSONALIZADOecredit_check_selected_ itemsvazio é recusado com422 CREDIT_CHECK_PROFILE_REQUIRES_SELECTION. - [x] CA3.
ADMIN_TENANTedita o preço deBOAVISTA_ESSENCIAL; o histórico de preços mostra as duas linhas (a antiga com suaeffective_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
ECONOMICOativo (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_ESSENCIALeDADOS_FINANCEIROSevoluem de "consultando" para o resultado com o custo no rodapé — o segundo só com CEP do inquilino ou do imóvel, senão nasceSKIPPEDporMISSING_DATA—, eCADINnasceSKIPPEDpor 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
FAILEDnã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 novoCreditChecké criado (a linha anterior permanece intacta, RN12). - [x] CA6a. Numa análise
APPROVED,REJECTEDouCANCELLED, reexecutar ou adicionar uma consulta é recusado com422 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
SUCCESSdeBOAVISTA_ESSENCIAL, recebe o resultado reaproveitado (reused_from_idpreenchido) sem custo e sem chamarApiFullClient(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
CreditChecknascemSKIPPEDcom 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_ate 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
CreditCheckcomcompleted_athá mais de 12 meses temraw_response = nulleraw_response_purged_atpreenchido depois do job diário rodar;GET .../rawdevolve"purged": truesem erro; onormalizedpermanece intacto. - [x] CA13. Acessar
GET .../rawgrava uma entrada emAuditLogcomaction="CREDIT_CHECK_RAW_ACCESS"e ouser_idde quem acessou.
Negativos de autorização (um por papel afetado, RN/seção 10):
- [x] CA14.
ADMIN_GLOBALchamandoGET /credit-finance/summaryrecebe403(semcredit_finance:read, subtraído explicitamente no seed, RN24). ChamandoGET /credit-analyses/{id}de uma análise de qualquer tenant, recebe200(permissão que já tinha antes desta spec), mas o campocredit_checksvemnull— a guarda de RN24 no service (ausência decredit_checks:readmais o guard explícito contrais_admin_global), não a ausência de permissão no endpoint em si, é o que fecha esse caminho. - [x] CA15.
AGENCY_USER(eAGENCY_MANAGER) chamandoGET /credit-analyses/{id}de uma análise da própria carteira recebe200comcredit_checksecredit_checks_total_estimatednull(RN23) — mesmo resultado quebureau_inquiry: nulljá 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_USERchamandoGET /credit-finance/summaryouPOST /credit-finance/syncrecebe403(credit_finance:read/credit_finance:syncausentes). - [x] CA17.
OPERATORchamandoGET /credit-analyses/{id}/credit-checks/{code}/rawrecebe403(credit_checks:read_rawausente) — masPOST .../reruncomconfirm_cost: truerecebe200, porquecredit_checks:reruncontinua concedido a este papel (10.1, preservandorenters:reanalyze_credit_bureaude hoje). - [x] CA18.
VIEWERchamando qualquer endpoint de reexecução/adição recebe403(credit_checks:rerunausente) —VIEWERsó lê. - [x] CA19. Uma análise de outro tenant, em qualquer endpoint de
credit-checksdesta spec, responde404, nunca403(10.3). - [x] CA20.
ADMIN_TENANTchamandoPATCH /credit-catalog/{code}recebe403— 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 (cincois_active=false, RN1a). Divergência do planejado:TenantSettingsganhou os campos novos, mas não perdeucredit_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 /settingsestendidos (RN2, RN4, RN5). Divergência: o catálogo global ficou emGET/PATCH /credit-catalog, sem o prefixo/adminque 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 viaCreditProfileItem(RN3), seleção exigida no personalizado (RN4), item inativo recusado com 404 (RN1a/10.3), token exigido por perfil, preço mascarado paraAGENCY_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 novaadmin/credit-catalog(SaaS admin) — 8 testes novos (4 emsettings/index.test.tsx, 4 emCreditCatalogPage.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 jobarqpor item (RN8a), reexecução/adição (RN9), idempotência (RN10),SKIPPEDsem token (RN11);Renterperde os quatro campos de bureau;GET /credit-analyses/{id}trocabureau_inquiryporcredit_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 camposcredit_bureau_type/credit_bureau_token→credit_check_tokenrenomeado, 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 semcredit_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 emtest_credit_check_service.py, maistest_api_full_client.py(11 testes) para oApiFullClient. - [x]
apps/web—CreditCheckCard/CreditCheckGroup(Parte II, 7.1.B), diálogo de adicionar consulta, drawer de payload bruto — testes emCreditCheckGroup.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çãoNONE(RN9/CA6a), exclusão deAGENCY_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, porqueProviderStatementEntry/StatementSyncService/POST .../syncsão RN15/RN16 e entram só na Fase 4, junto do endpoint que os usa; até lá,unit_price_actualfica semprenulle o relatório de divergências sempre vazio, não fingindo conciliação que não existe), card deoperations-center(RN21) - [x]
apps/api— testes: RN17a (o que conta e o realnull), projeção, RN21 (efetivo, mesmos dias, trava de fim de mês), período invertido, isolamento nosummary, noby-iteme no card doADMIN_GLOBAL, negativos de ANALYST/OPERATOR/AGENCY_USER/ AGENCY_MANAGER/ADMIN_GLOBAL — 22 casos emtest_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 emcredit-finance/index.test.tsx(desktop e mobile) e 4 emSummaryCards.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 emtest_credit_statement_sync.py. - [x]
apps/web— "Sincronizar agora" de verdade (só comcredit_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.tscom a sincronização (sandbox) e oVIEWERsem o botão.
Ao fechar cada fase¶
- [ ]
make preflight - [ ]
make testemake 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
FAILEDdepois do commit da requisição — lia na mesma sessão, que via oflush, e a mutação que tirava o commit passava verde. O teste passou a rodar com a sessão emrequest.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
PENDINGantes 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 originalFAILEDagora fecha a espera junto (fechar_quem_espera, antes privada do worker). - Minor, corrigido: um passo depois do commit que falhasse virava
500para uma requisição já gravada e impedia os seguintes; agora fica registrado no log e os demais rodam. O contrato do mecanismo ganhou testes emtest_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 BYdos 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 semtry, passo rodando nofinally(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). ACommitRouteganhou um passo "depois do commit" (depois_do_commit), e o agendamento do job passou para ele; a falha da fila, que antes fechava a consultaFAILEDdentro da transação, agora a fecha com o próprio commit. Teste com a sessão da requisição emrequest.state.db, como em produção, afirmando que o commit vem antes da fila na submissão, noadde norerun. - 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
rerunnão aplica a RN2 — só alcançável reexecutando um QUODSKIPPEDdepois de adicionar o Boa Vista, e o QUOD está inativo (a RN9 fala da RN2 só no "adicionar"); o cartãoITEM_UNAVAILABLEnã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 oGET /settingsnã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
PENDINGpara 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 ignoraFOR UPDATE); o limite e a saída têm teste. - Important (RN6): a exclusão de
test_tenant_settings_credit_bureau.pylevou os testes do token write-only e do sandbox. Voltaram, sobre o campo novo: o token nunca volta noPATCHnem noGET,credit_check_token_configurednos 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
SKIPPEDnão conta para a exclusão mútua;rerun/addrecusam principal sem CPF (422 CREDIT_CHECK_RENTER_NOT_ELIGIBLE), como a submissão; o cartãoFAILEDnã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 temrenters:restore; o link para Parâmetros só aparece para oADMIN_TENANT(os demais leem o recado); a espera de um item desligado não manda reexecutar; o Swagger do catálogo documenta o422de 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 diziaPOSTno 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,SKIPPEDna exclusão mútua,FAILEDno 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):
addererunrecusavam só os status decididos, então uma análise emDRAFTaceitava 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..._DECIDEDnão descreve um rascunho. Teste deDRAFTnos dois endpoints. - Critical (T2, RN24):
rerun,adderawnão tinham a guarda doADMIN_GLOBALque 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á aoADMIN_GLOBAL, não o da fixture. Tirarcredit_checks:read_rawda 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 deVIEWEReANALYST. - Important (10.3):
rerunnão conferia se o item continuava ativo, e o worker também não. Os dois endpoints devolvem404para item desligado ou sem endpoint; o worker fecha comoFAILEDo job de um item desligado depois de enfileirado. - Important (RN11/RN20): o cartão
SKIPPEDnã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
FAILEDapontando "Reexecutar". Testes do sucesso, da falha e da trava desligada. - Minor corrigidos: o Swagger de
/settingse do schema dizia "ausente" onde a RN25 e o código devolvemnull; o Swagger dererun/addganhou os erros que faltavam; o seed deBOAVISTA_ESSENCIALpassou a gravar os campos reais da RN7 (a migration da Fase 4 corrige o dado, e odowngradeo 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 oAGENCY_MANAGERno pytest; oadddo 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 doid(antes,length >= 1); ofinallydecredit-check-config.spec.tsdevolve a seleção personalizada; os comentários da 0050 emconfig.pye 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 norerune 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 semid(coluna anulável;NULLnão conflita nounique), como a RN15 diz. - Important (T1, RN20): o disjuntor no
rerune noadd, e a janela de 10 minutos do armazenamento de produção, não tinham teste. Dois testes de endpoint e um doRedisStorecom 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_USERna sincronização e deANALYST/OPERATORno teste de token; duas sincronizações simultâneas do mesmo tenant podiam responder 500 nounique— cada lançamento grava numSAVEPOINTe 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 gravavamrequested_atcomo "agora" em UTC. Corrigido: "hoje" fixo em todo otest_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_GLOBALnão tinha teste de isolamento — o zero dele depende do filtro incondicional de tenant. Teste novo; red-green trocando o filtro peloif tenant_id:doBaseRepository. - Important (T1): a trava de fim de mês da RN21 (
mincom 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.pyainda dizia "sempre0"; o teste deby-itemagora pede a página 2 comper_page=1;OPERATOReAGENCY_MANAGERganharam 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 perfilECONOMICOem 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 peloPageHeader, e o menu segue com a string literal — é como os demais itens denav-groups.tsxestã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_totalnullquando 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 dublandomatchMediae 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 (
monkeypatchsobrehoje_local) — e a variação mudou de regra no caminho (decisão do usuário, Registro). - Important (Q/e2e): o
finallydo e2e restaurava só o perfil, e o cabeçalho dizia que desfazia o token. Corrigido: restaura o perfil e, se o original eraPERSONALIZADO, 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-itemno mesmo teste; o403doADMIN_GLOBALpassou 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_LOCALduplicado trocado peloFUSO_LOCALpú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 testaAGENCY_USEReAGENCY_MANAGERcom o título certo; oFilterPill/FilterSelectde período não oferece mais "Todos" (allLabel={null}), e o desktop usaFilterBar/FilterSelect; os números do resumo reusam oSummaryCard; variação no formato pt-BR; o card não é clicável semcredit_finance:read; o vazio não oferece Parâmetros aoVIEWER; 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), comcurrent_month_countno contrato. - Minor, sem mudança:
month_over_monthsem guarda de RN24 — oADMIN_GLOBALchega comtenant_idNone, 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
403para papel semcredit_checks:rerunnos endpointsrerun/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
404para análise de outro tenant nos três endpoints novos (CA19). Corrigido:test_endpoints_novos_recusam_analise_de_outro_ tenant_com_404, cobrindorerun/add/raw. - Important (A2/RN11): o card
SKIPPEDnão trazia o link para Parâmetros que RN11/ CA8 exigem. Corrigido emCreditCheckGroup.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(viaadd) eCREDIT_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:readnorbac_seed.pycitavarenters:reanalyze_credit_bureau— quem de fato substitui essa ação écredit_checks:rerun. Corrigido. - Minor (Q2):
credit_bureau_sandbox/README.mdainda referenciavacredit_bureau_service.py/test_credit_bureau_service.py, removidos nesta fase. Atualizado paraapi_full_client.py/test_api_full_client.py. - Minor (Q1):
show.tsxcalculavacanRerunCreditCheck/canReadRawCreditCheckpelo atalhocan(), que concede tudo aADMIN_GLOBAL— inconsistente em princípio com RN24 (inofensivo hoje porquecredit_checksjá vemnullpara esse papel). Corrigido para checar a permissão direto, sem o atalho. - Minor (Q4): linha de import em
credit_analyses.pyacima de 120 colunas. Quebrada em múltiplas linhas.
Fase 2, rodada 4 — achado e correção:
- Important (Q3):
CreditAnalysisService.search()chamavabuild_response_fieldsdentro do loop de montagem da página — uma consulta acredit_checkspor análise listada, em vez do padrão de carga em lote que o mesmo método já usa paraproperties_map/creators_map/approvers_map/guarantees_map. Corrigido:CreditCheckRepository.latest_per_item_bulk()(uma consulta para todas as análises da página) eCreditCheckService.build_response_fields_bulk(), chamado uma vez por página emsearch(). 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=falseera aceito na seleção personalizada (RN1a/ 10.3 exigem 404). Corrigido:validate_codes_existagora filtra poris_active; teste novotest_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 emtenant_settings_service.py, mesma regra da RN2 da 0050; teste novotest_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 deCREDIT_CATALOG_ITEM_NOT_FOUNDcomo a spec documenta. Corrigido com um erro dedicado (_catalog_item_not_found), código literal. - Important (A1):
credit_check_composition_total_estimateestava documentado na seção 12 mas não existia no schema nem era calculado. Adicionado aTenantSettingsResponsee computado em_to_response, mascarado (null) paraAGENCY_USER/AGENCY_MANAGERcomo 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_codescom linhas reais. Adicionadotest_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 mesmoPATCH /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.bre o guia de autenticação emdoc.apifull.com.br/guides#authentication. Os links que apontavam para Parâmetros (o cartãoSKIPPEDsem 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
SUCCESSdeFAILED; 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 levantavamNotImplementedError(viram documentação no/wiki); edados_financeirostinha cópia própria do caminho comum de chamada, que já divergira no timeout — passou a usar_consultar, como os outros quatro. DosMinor: 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
SKIPPEDpedindo um cadastro que não mudaria nada. O reaproveitamento passou a vir primeiro. -
DADOS_FINANCEIROSfoi 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_financeiroscomrenda_estimada_v2,patrimonio_estimado,classe_socialepoder_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 ostatement_labelque 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
SKIPPEDcomMISSING_DATAe 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, eQUOD_PFambíguo entre/api/pf-quod-consulta,pf-quod-score,pf-quod-negativo-*eic-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 paranormalized(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 aEM IMPLEMENTAÇÃOe o merge espera esses exemplos. Pedido também, para cada consulta, o nome exato que aparece no extrato de créditos — é ostatement_labelcom 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 emGET /settingspassou a trazerprofiles, 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 (noWrapnum 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, naCommitRoute) é 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 emPENDING/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
SKIPPEDnã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
SKIPPEDaponta para lá. - RN10 — a consulta em andamento: espera e reaproveita. A RN10 dizia que "nem
PENDINGem andamento é duplicado" sem dizer o que a segunda análise recebe, e o código só enxergavaSUCCESS— duas análises do mesmo locatário submetidas juntas pagavam duas vezes. Perguntado, o usuário escolheu esperar: a nova nascePENDING, 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, sobcredit_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..._DECIDEDdescreveria 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_GLOBALnos endpoints decredit_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 doSKIPPEDpassa a ser dado. Coluna nova emcredit_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}comis_active: truenum item deendpointnulo é422. Antes, oADMIN_GLOBALconseguia deixar um item que a submissão marcavaSKIPPEDe a consulta avulsa deixavaFAILED.
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) comGET /api/credits/statementeGET /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_idquando houvesse. Não há. O casamento ficou por item (o nome da consulta no extrato, guardado no catálogo emstatement_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) estatement_label. Entraram na migrationc4f1a9d2e7b3, comserver_defaultnos 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. FAILEDcobrado vira divergência, não custo. Se a API Full cobrar uma consulta que falhou, o lançamento não casa (sóSUCCESScasa, 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,PENDINGeRUNNING—FAILEDeSKIPPEDficam fora (virou a RN17a). A primeira versão contava tudo, e a tela mostrava "CADIN — 16 no período" para dezesseis consultasSKIPPEDque 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_totalvemnullsem conciliação. Ocoalesce(..., 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). OVIEWERlê 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: 1eminWidth: 0, oSummaryCardespremia todos os cards numa linha só em 375px, com título e chips cortados; o card desta spec, o quinto, piorava. Corrigido noSummaryCard(flex-basisde 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:
MobileFilterBarcom busca opcional (a tela de relatório não tem o que buscar, e o campo aparecia sempre) eallLabel={null}noFilterPill/FilterSelectpara o filtro de período, que sempre tem valor. Quem não passa nada continua como estava; os.prompt.mddos dois dizem isso. -
O card do Centro de Operações não é clicável para quem não tem
credit_finance:read. O card é deoperations_center:read(RN21) e o relatório é decredit_finance:read; oANALYSTvê 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 .../syncestã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
937f3e75d39dcriavacredit_checks.created_at/updated_atsemserver_default, ao contrário do queTimestampMixinjá 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 todoINSERT(viaCreditCheckRepository.create_isolated, que não passacreated_atexplicitamente, confiando no default) falhando comNotNullViolationError, silenciosamente engolido pelo própriotry/exceptdecreate_isolated(RN8a: uma falha de gravação não pode derrubar a submissão) — a análise submetia normalmente, só que comcredit_checks: []sempre, em qualquer tenant. Só apareceu rodando o e2e de verdade contra o dev-remote — a suíte depytest(SQLite) não pegou porque lá o teste usaBase.metadata.create_ all(), que gera o DDL a partir do model (com oserver_defaultcorreto), 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), comdowngrade/upgradereexecutados 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.
CreditCheckGroupimplementava 7.3 à risca: comcredit_checks: [](perfilNONE, 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 emSUBMITTED/IN_ANALYSIS/PENDINGmesmo sem nenhumCreditCheckainda, 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 (semcredit_checks:rerun). RN9, 7.3, CA6a e o contrato dererun/add(seção 12,422 CREDIT_CHECK_ANALYSIS_DECIDED) foram atualizados juntos. O e2e de "adicionar avulso a partir de perfilNONE", removido na primeira versão desta fase por não ter onde clicar, volta a existir e a passar — a mesma composiçãoNONEque antes escondia o bloco inteiro agora só esconde os cartões, não o botão. credit_finance:read/credit_finance:syncnã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 dependency_types:reademrbac_seed.pyjá 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_rawentram 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:readcomo ó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_permissiondo router,exigir_permissaodo 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 mapeavaBOAVISTA_ESSENCIALpara 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) produzscore,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 deGET /credit-analyses/{id}(seção 12) para os nomes reais, antes de escreverApiFullClient.boavista_essencial()— divergência técnica descoberta relendo o parser já confirmado, não decisão de negócio; a linha deQUOD_PFcontinua abstrata de propósito (RN1a, endpoint ainda não confirmado). - CA4 presumia os três itens do perfil
ECONOMICOcom endpoint confirmado — não é o caso hoje: a migrationeb1df26c8862(Fase 1) semeou sóBOAVISTA_ESSENCIALcomis_active=true/endpointpreenchido;DADOS_FINANCEIROSeCADINnascemis_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 nascemSKIPPED) — 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_tokennã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 emCreditCheck(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/adminque 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
Selectbiná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 (Switchem modo personalizado) falhou —data-testidnum<Switch>cru do MUI vai para um<span>interno, não para o<input>, e o clique douser-eventnão alcançava oonChange. Corrigido comslotProps={{ 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. CurrencyTextFieldganhousizeedata-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
eb1df26c8862verificada de duas formas: o--sql(modo offline) falha noop.bulk_insertdecredit_catalog_items— limitação conhecida do SQLAlchemy/ Alembic, que não sabe renderizarJSONcomo 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:
- Só entra o que um revisor conferiu — não o que você conferiu ao escrever. O ponto é aproveitar trabalho já pago por uma rodada, não abrir atalho para afirmar sem prova.
- Mexeu na frase da spec, a linha sai da tabela. A afirmação volta a ser conferível. Sem isso a tabela vira o oposto do portão: uma afirmação carimbada como verdadeira porque um dia foi.
Feature sem afirmação sobre o repositório não precisa da seção — apague-a.