0007 — Análise de Crédito: rename completo (Proposal → CreditAnalysis) + visão mobile¶
| Status | CONCLUÍDA |
| Branch | spec/0007-analise-de-credito-linguagem-e-mobile |
| Criada em | 2026-07-30 |
| Apps afetados | api · web · e2e |
Status:
RASCUNHO→APROVADA→EM IMPLEMENTAÇÃO→CONCLUÍDA
1. Problema¶
Duas dívidas na mesma área, ambas visíveis para quem usa:
Linguagem. O negócio chama de Análise de Crédito; o sistema chama de Proposta. O menu já foi renomeado (nav.creditAnalysis = "Análise de Crédito"), mas as telas por baixo seguem dizendo "Proposta": título ("Propostas"), botão ("Nova proposta"), vazio ("Nenhuma proposta encontrada"), diálogos ("Cancelar proposta") — 12 chaves i18n — além de cabeçalhos de tabela hardcoded em português na lista (Código, Status, Locatário, Proprietário, Imóvel, Garantia). No código, a entidade é Proposal de ponta a ponta (rota /proposals, endpoint /api/v1/proposals, tabela proposals, permissões proposals:*). O usuário lê um nome, o time fala outro e a documentação precisa traduzir — atrito permanente.
Mobile. As telas de Análise de Crédito não têm tratamento mobile. A lista usa <Table> puro (estoura a largura no celular) e os formulários de criação/edição usam grade de duas colunas com o resumo financeiro em painel lateral. Renters, imóveis e imobiliárias já ganharam a versão mobile na spec 0004 (useIsMobile + MobileListCard) — a área mais usada da operação ficou de fora.
2. Objetivo¶
Fazer o sistema falar Análise de Crédito em todas as camadas (interface, URL e código) e deixar as quatro telas do módulo usáveis no celular, no mesmo padrão dos outros cadastros.
3. Fora de escopo¶
- Mudança de comportamento ou regra de negócio: é rename + responsividade. Nenhum status novo, nenhuma validação nova, nenhum cálculo alterado.
- O modelo-alvo de 10 papéis area-scoped (
navigation-brief.md) — as permissões são renomeadas, não redesenhadas. - Portal da imobiliária e as entregas E2/E3 do ROADMAP.
- Compatibilidade retroativa da API: os endpoints antigos não são mantidos como alias (ver RN9) — o cliente é o próprio
apps/web, versionado junto.
4. Regras de negócio¶
Rename (linguagem, URL e código)¶
- RN1. A entidade hoje chamada
Proposalpassa a serCreditAnalysis— o caso de análise de crédito (o dossiê que nasce emDRAFTe caminha atéAPPROVED). - RN2. A entidade hoje chamada
Analysis(o parecer do analista dentro do caso) passa a serDecision. Isso resolve a colisão: sem o rename duplo, ficariaCreditAnalysiscontendoAnalysis, indistinguíveis na leitura. AnalysisStatus→DecisionStatus;Analysis*nos schemas →Decision*.CreditRecommendationmantém o nome (já é claro e não colide).- RN3. Renomes de banco (uma migration, com
ALTER TABLE ... RENAME, preservando dados e FKs):
| De | Para |
|---|---|
proposals | credit_analyses |
proposal_participants | credit_analysis_participants |
proposal_status_history | credit_analysis_status_history |
analyses | decisions |
coluna proposal_id (em analyses/decisions, contracts, documents, credit_analysis_participants, credit_analysis_status_history) | credit_analysis_id |
- RN4.
ProposalStatus→CreditAnalysisStatus. Os valores do enum não mudam (DRAFT,SUBMITTED,IN_ANALYSIS,PENDING_DOCS,APPROVED,REJECTED,CANCELLED) — renomear valores exigiria migrar dados sem ganho. - RN5. Endpoint
/api/v1/proposals→/api/v1/credit-analyses(kebab-case, como o resto da API). Sub-recursos acompanham:/{id}/analysis→/{id}/decision. - RN6. Permissões
proposals:*→credit_analyses:*eanalysis:*→decisions:*(17 permissões). A migration precisa atualizar as permissões existentes e os vínculos dos papéis — perder isso tira acesso de todo mundo. - RN7. Rota do front
/proposals→/credit-analyses(list/create/edit/show), com onamedo resource no Refine acompanhando. Sem redirect das URLs antigas (decisão: cliente único, versionado junto). - RN8. Textos da interface: nenhuma tela exibe "Proposta"/"proposta". As 12 chaves i18n passam a "Análise de Crédito"/"análise"; os cabeçalhos hardcoded da lista viram chaves i18n (PT/EN).
- RN9. Sem alias de compatibilidade: as rotas e os endpoints antigos deixam de existir. O
apps/webe oapps/e2esão atualizados no mesmo commit. - RN10. Nenhuma mudança de comportamento. Status, transições, cálculo (Pacote/LMI/Custos de Saída da spec 0006) e permissões efetivas por papel ficam idênticos — só os nomes mudam.
Visão mobile¶
- RN11. Lista: abaixo do breakpoint mobile, a tabela é substituída por cards (
useIsMobile+MobileListCard), no mesmo padrão de renters/imóveis/ imobiliárias. Cada card mostra código, status, locatário, imóvel e valor. - RN12. Criação e edição: formulário em coluna única no mobile (a grade de duas colunas colapsa), na mesma linha da spec 0004.
- RN13. Resumo financeiro: no desktop segue como painel lateral; no mobile vira uma seção colapsável, iniciando recolhida, para não empurrar os campos para baixo. Um toque abre.
- RN14. Detalhe (show): seções em coluna única no mobile, usando
MobileDetailSectiononde couber (padrão já usado emrenters/show). - RN15. As ações principais (submeter, aprovar, rejeitar, cancelar) ficam alcançáveis com o polegar no mobile e não podem ficar escondidas atrás de scroll horizontal.
Permissões: inalteradas em efeito (RN6/RN10) — quem podia criar/aprovar continua podendo, sob os nomes novos.
5. Impacto por app¶
apps/api¶
Alcance medido: 1.049 ocorrências de Proposal/proposal em 46 arquivos.
- Models:
proposal.py→credit_analysis.py(CreditAnalysis,CreditAnalysisStatus,CreditAnalysisStatusHistory);proposal_participant.py→credit_analysis_participant.py;analysis.py→decision.py(Decision,DecisionStatus,CreditRecommendation). - FKs a acertar:
decisions,contracts,documents,credit_analysis_participants,credit_analysis_status_history. - Schemas / repositories / services / routers: renomeados em bloco (
proposal_service→credit_analysis_service,analysis_service→decision_service, etc.), com Swagger atualizado (summary,description,response_description) eTAGS_METADATAemapp/main.py. - RBAC:
rbac_seed.pycom os nomes novos + migration atualizandopermissions.namee preservandorole_permissions(RN6). - Migration Alembic:
ALTER TABLE/ALTER COLUMN ... RENAME(não recriar tabela — os dados e as FKs precisam sobreviver), mais oUPDATEdas permissões.downgradesimétrico. - Testes: 5 arquivos citam
proposal; renomear e manter a cobertura. A suíte precisa continuar em 173 passed — é a rede que prova que o rename não mudou comportamento (RN10). wiki/docstrings: AutoAPI segue as docstrings; atualizar as que citam "proposta".
apps/web¶
Alcance: 25 arquivos citam proposal.
src/pages/proposals/→src/pages/credit-analyses/(index, create, edit, show, api.ts, components) com os componentes renomeados (ProposalListPage→CreditAnalysisListPage, etc.).App.tsx: resourceproposals→credit_analyses, rotas/credit-analyses/*.components/custom-sider/index.tsxprecisa acompanhar (sider é hardcoded — ver CLAUDE.md do web).- i18n: namespace
proposals.*→creditAnalyses.*, textos sem "proposta", e as chaves novas para os cabeçalhos da lista (PT/EN). - Mobile (RN11–RN15):
useIsMobile+MobileListCardna lista; coluna única em create/edit; resumo financeiro colapsável;MobileDetailSectionno show. data-testid(renomeados/novos):credit-analyses-new-button,credit-analysis-row-{id},credit-analysis-card-{id},credit-analysis-form,credit-analysis-form-submit,credit-analysis-financial-summary-toggle.- Responsividade & PWA: é o coração da parte 2. Sem impacto de PWA além do cache normal de assets (o banner da spec 0005 avisa a nova versão).
apps/e2e¶
3 specs citam proposal — proposals-analysis.spec.ts (renomear para credit-analyses.spec.ts), pacote-locaticio.spec.ts e mobile-shell.spec.ts (ajustar rotas/testids).
- Cenários novos: lista em cards no mobile, formulário em coluna única e resumo financeiro colapsável (fechado por padrão, abre ao toque).
- Rodar em desktop e mobile.
6. Contrato de API¶
Quebra de contrato assumida (RN9). Antes → depois:
GET /api/v1/proposals → GET /api/v1/credit-analyses
POST /api/v1/proposals → POST /api/v1/credit-analyses
GET /api/v1/proposals/{id} → GET /api/v1/credit-analyses/{id}
PATCH /api/v1/proposals/{id} → PATCH /api/v1/credit-analyses/{id}
POST /api/v1/proposals/{id}/submit → POST /api/v1/credit-analyses/{id}/submit
POST /api/v1/proposals/{id}/analysis/approve
→ POST /api/v1/credit-analyses/{id}/decision/approve
O corpo das requisições/respostas mantém os campos, exceto o rename de proposal_id → credit_analysis_id onde ele aparece (ex.: criação de contrato).
Permissões no token/perfil: proposals:create → credit_analyses:create, analysis:approve → decisions:approve, e assim por diante.
7. Critérios de aceite¶
- [x] CA1. Nenhuma tela do módulo exibe "Proposta"/"proposta" — título, botão novo, estado vazio, diálogos e cabeçalhos da lista dizem "Análise de Crédito"/"análise".
- [x] CA2. A URL é
/app/credit-analyses(list/create/edit/show) e o item do menu leva até lá. - [x] CA3.
GET /api/v1/credit-analysesresponde 200;/api/v1/proposalsnão existe mais (404). - [x] CA4. Um
ANALYSTcontinua conseguindo submeter e aprovar, e umVIEWERcontinua sem conseguir — as permissões renomeadas preservam o acesso efetivo. - [x] CA5. Os dados sobrevivem à migration: análises, participantes, histórico, pareceres, contratos e documentos anteriores seguem consultáveis e ligados corretamente.
- [x] CA6. No mobile (375px) a lista mostra cards, sem scroll horizontal.
- [x] CA7. No mobile, criação e edição ficam em coluna única, e o resumo financeiro aparece recolhido, abrindo ao toque.
- [x] CA8. No mobile, submeter/aprovar/rejeitar/cancelar estão acessíveis sem scroll horizontal.
- [x] CA9.
make testsegue verde (173 passed) e o e2e passa em desktop e mobile — prova de que o rename não alterou comportamento (RN10).
8. Plano de implementação¶
Ordem obrigatória: api → web → e2e. O rename vem antes do mobile (o mobile mexe nos arquivos já renomeados).
- [x]
apps/api— models (CreditAnalysis,Decision, participantes, histórico) + FKs - [x]
apps/api— schemas → repositories → services → routers, com Swagger eTAGS_METADATA - [x]
apps/api—rbac_seedcom permissões novas - [x]
apps/api— migrationl5n7p9r1u3v4:RENAMEde 4 tabelas e das colunasproposal_id,UPDATEdas permissões preservando os vínculos de papel;downgradesimétrico - [x]
apps/api— testes renomeados; suíte em 173 passed (igual ao pré-rename) - [x]
apps/web—pages/proposals→pages/credit-analyses,App.tsx, sider, i18n (namespacecreditAnalyses.*+ textos sem "Proposta"), testids em kebab - [x]
apps/web— mobile: cards na lista, coluna única nos formulários, resumo financeiro colapsável, detalhe em seções - [x]
apps/e2e— 3 specs ajustadas (rotas, testids,/decision); 42 passed - [x]
apps/e2e— cenários mobile novos (credit-analyses-mobile.spec.ts) - [x]
make teste tsc do web limpos; sem apontamento novo de lint nos arquivos tocados - [x] Validado no browser em desktop e mobile
9. Decisões em aberto¶
Resolvidas com o usuário (2026-07-30):
- [x] Alcance do rename — interface + URLs + código/API (o refactor completo que o ROADMAP prevê).
- [x] Colisão de nomes —
Proposal→CreditAnalysiseAnalysis→Decision, para não ficar "CreditAnalysis contendo Analysis". - [x] Fatiamento — tudo na 0007 (rename + mobile juntos), ciente de que o diff fica grande.
- [x] Telas — lista, criação, edição e detalhe.
- [x] Lista no mobile — cards, reusando
MobileListCard. - [x] Resumo financeiro no mobile — colapsável, recolhido.
- [x] Compatibilidade — sem alias das rotas/endpoints antigos (RN9).
Risco assumido e registrado: é o maior refactor da base até aqui (1.049 ocorrências, 46 arquivos na API, 4 tabelas, 17 permissões) entregue junto de uma mudança de UI. O ponto mais perigoso é a migration de permissões (RN6): errar ali tira acesso de todos os papéis — CA4 existe para pegar isso. A rede de proteção é a suíte de 173 testes + e2e (CA9).
10. Registro¶
Parte 2 — mobile (2026-07-30)¶
Entregue como planejado: cards na lista (MobileListCard), coluna única nos formulários, resumo financeiro colapsável e detalhe em MobileDetailSection.
Divergências do planejado:
- Resumo financeiro fica acima dos campos no mobile, não no fim da página. A grade já colapsava para uma coluna, então o painel caía no rodapé — onde "colapsável" não resolve nada. Subiu para logo abaixo do cabeçalho, recolhido: cumpre a razão de ser da RN13 ("não empurrar os campos para baixo") e deixa o número a um toque.
- Ações do detalhe migraram para barra fixa no rodapé no mobile. O
PageHeaderportalizaactionspara a top bar, que tem ~40px de altura — três botões não caberiam (RN15). O mesmo JSX serve aos dois lugares viaflexpor breakpoint. - Botão "Nova análise de crédito" virou ícone no mobile. Com o rótulo inteiro na top bar, o título da página era truncado para "An… Gere…". Só ícone, nome no
aria-label. - Footers dos formulários empilham no mobile (primária em cima, largura total; secundárias abaixo) — três botões lado a lado não cabem em 375px.
Pendências da parte 1 que estavam em aberto e foram fechadas aqui:
- Cabeçalhos da lista estavam hardcoded em português (
Código,Status,Locatário,Proprietário,Imóvel,Garantia,Criado por,Aprovado por) — viraram chaves i18n (PT/EN), como a RN8 exigia. - Sobras de "Proposta" na interface:
usePageTitle("Propostas"), contador "proposta/propostas", título do detalhe (Proposta P2026…), textos deedit.tsx, rótulo do resource emApp.tsx,TenantListPage,WorkQueueItem, tooltip emcontractse texto dedocument-types. - Catálogo EN tinha 25 linhas com o artefato do rename mecânico (
"No creditAnalyses found","CreditAnalyses","This creditAnalysis has…").
Três regressões da parte 1 encontradas na verificação¶
Nenhuma tinha a ver com a parte 2 — apareceram porque make test e o browser foram exercitados de verdade.
- Suíte da API estava em 6 failed / 167 passed, não nos 173 registrados. Causa: o espelho de permissões do
tests/conftest.pyficou com o recursodecision(singular) enquantorbac_seed.pye os routers usamdecisions. Oif key in perm_mapengolia a chave inexistente em silêncio, então oANALYSTnão recebiadecisions:createe todo teste de parecer dava 403. Corrigido emconftest.py(_ALL_PERMISSIONSe_SYSTEM_ROLE_PERMISSIONS),test_rbac_security.pyetest_operations_center.py→ 173 passed. O banco real estava correto (credit_analyses9,decisions4): a migration da RN6 fez seu trabalho — era desalinhamento só do espelho de teste. - Painel de documentos da Análise de Crédito não filtrava. O front mandava
entity_type=creditAnalysis(camelCase) e a API esperacredit_analysis; ocol_map.get()dodocument_repodevolviaNonee a consulta seguia sem filtro nenhum, listando documentos de todas as entidades do tenant. Antes da parte 1 o valor eraproposale casava. Corrigido emshared/api/documents.tseshow.tsx. - Rótulos e ícones do Centro de Operações e o filtro da Auditoria usavam
creditAnalysis/analysis, mas a API emitecredit_analysis/decisionementity_type— os rótulos caíam no fallback e o filtro de auditoria não casava nenhum registro. Corrigido emWorkQueueItem,WorkQueue,ActivityItemeaudit-logs.
Bug de renda declarada — reportado pelo usuário em tela real¶
Depois da entrega, o usuário mandou print do celular: renda preenchida (R$ 8.000,00) e a API recusando com "ao menos um inquilino deve ter a renda mensal declarada". Causa: o create.tsx guarda a renda num único state que, após o onBlur, passa a conter o valor já formatado ("R$ 8.000,00") — e o envio parseava com parseFloat cru:
| Onde | Parser | Resultado com "R$ 8.000,00" |
|---|---|---|
buildParticipants LESSEE | parseFloat(v) | NaN → null → API recusa |
buildParticipants CO_RESPONSIBLE | replace(",", ".") + parseFloat | 8 → análise criada com renda errada, sem erro |
onFocus do inquilino | idem | 8.000 vira 8 ao reabrir o campo |
O caso do corresponsável é o mais grave: não dá erro, só grava a renda errada e contamina o índice de capacidade financeira.
Corrigido usando o parseBRLInput do próprio arquivo (aceita "8000" e "R$ 8.000,00") nos três pontos, mais os onChange de condomínio e IPTU — que usavam o parser ingênuo enquanto o edit.tsx já usava o correto, então digitar 1.500,00 no condomínio virava R$ 1,00 no Pacote Locatício.
Pré-existente desde efcc33c, anterior a esta spec; nenhuma linha de parsing foi tocada pela parte 2 — apareceu porque o formulário foi finalmente exercitado num celular real.
Coberto por teste e2e novo que dirige o formulário mobile e inspeciona o corpo do POST (declared_income === 8000), e o create.tsx ganhou os data-testid que faltavam (imóvel, inquilinos, aluguel, renda por inquilino) — exigência do CLAUDE.md do web que o arquivo não cumpria.
Dois defeitos vizinhos, corrigidos a pedido do usuário¶
Encontrados ao investigar o bug da renda; o usuário mandou corrigir os dois.
1. Busca de imóvel não achava endereço com número. O filterOptions tirava espaços/pontos/hífens só da consulta quando ela tinha 3+ dígitos — recurso pensado para casar CEP mascarado (01310-100 ↔ 01310100). Só que o alvo continuava com espaços, então "Rua E2E 0007" virava "ruae2e0007" e não casava com "rua e2e 0007". Buscar por endereço + número ("Paulista 1000") não devolvia nada. Agora a forma compacta é aplicada aos dois lados, e a comparação literal continua valendo — o CEP mascarado segue funcionando. Verificado com 11 casos; 3 que retornavam 0 agora acham.
2. Separador de milhar digitado à mão virava centavo. parseFloat("1.500") é 1.5: um condomínio de R$ 1.500 entrava como R$ 1,50 no Pacote Locatício — a base da taxa, do LMI e dos Custos de Saída (spec 0006). Silencioso, direto no dinheiro.
O parseBRLInput estava duplicado em create.tsx e edit.tsx, com o mesmo defeito nas duas cópias. Passou a viver único em shared/utils/formatUtils.ts, com regra explícita de pt-BR:
| Entrada | Antes | Agora |
|---|---|---|
"1.500" | 1.5 | 1500 |
"1.500.000" | 1.5 | 1500000 |
"1.234.567,89" | 1234567.89 | 1234567.89 |
"1500.50" (US) | 1500.5 | 1500.5 |
"1.5" | 1.5 | 1.5 |
A regra: vírgula manda (é o decimal do pt-BR); sem vírgula, pontos em grupos de 3 (/^\d{1,3}(\.\d{3})+$/) são milhar e caem; fora do agrupamento, o último ponto é decimal — assim "1.5" e "1500.50" não regridem. Verificado com 16 casos.
Ambos cobertos por teste e2e.
Unificação do formatBRL (feita depois, a pedido do usuário). A função estava duplicada byte a byte em create.tsx e edit.tsx e divergia do fmtBRL compartilhado: devolve "" quando não há número, em vez de "R$ 0,00". A diferença é intencional e necessária — é o valor exibido num campo, que nasce vazio; com fmtBRL o campo de aluguel apareceria preenchido com "R$ 0,00" ao abrir o formulário.
Então não era caso de deletar uma e usar a outra: são dois contratos. Ambas passaram a viver em shared/utils/formatUtils.ts com os papéis nomeados e documentados:
| Função | Para quê | Sem valor |
|---|---|---|
fmtBRL | valor conhecido (tabela, detalhe, resumo) | "R$ 0,00" / "R$ NaN" |
fmtBRLInput | conteúdo exibido de um campo | "" |
parseBRLInput | normaliza o que o usuário digita | "" |
parseBRLNumber | idem, já como number \| null | null |
O sufixo Input marca o par voltado a formulário. As 16 chamadas locais passaram a apontar para a compartilhada, e a troca de toLocaleString pelo Intl.NumberFormat em cache foi conferida em 14 casos (incluindo negativos, zero e arredondamento) — saída idêntica.
parseBRLNumber foi usado nos dois pontos de declared_income do create.tsx, onde expressa exatamente o que a API espera. Não foi aplicado no edit.tsx: lá o state guarda o valor já normalizado, e reparsear introduziria o erro inverso — "100.000" (cem reais, normalizado) viraria cem mil. Os dois arquivos têm contratos de state diferentes, e isso agora está comentado no código.
Bug pré-existente corrigido de carona: renters/show.tsx mandava entity_type=person, mas a API só conhece renter — o painel de documentos do inquilino tinha o mesmo defeito de filtro desde o commit que o introduziu (e8fd38f), anterior a esta spec. Um caractere para consertar; deixar quebrado ao lado do irmão corrigido seria pior.
Verificação¶
- API: 173 passed (rodado dentro de
garantora-devremote-api-1— o container tem o código embutido, sem mount, então os testes precisam ser copiados comdocker cp; o host não temuv). - Lint: 40 apontamentos do ruff nos arquivos de teste tocados antes e depois — nenhum novo (todos E501/F401/I001 herdados).
tscdo web limpo. - e2e: 17 passed (
credit-analyses-mobile,credit-analyses,mobile-shell,pacote-locaticio) — 14 da entrega inicial + 3 de regressão (renda declarada, busca de imóvel com número, separador de milhar). - Browser real (Playwright contra o dev-remote, web rebuildado): 4 telas × 2 viewports (1440px e 375px) —
scrollWidth - clientWidth == 0nas 8, e as capturas confirmam cards, resumo recolhido, coluna única e barra de ações.