0055 — O acerto com a imobiliária: o que ela recolheu, o que repassou, e o mês fechado¶
| Status | CONCLUÍDA |
| Branch | spec/0055-repasse-por-imobiliaria |
| Criada em | 2026-10-01 |
| Apps afetados | api · web · e2e |
| Revisão do problema | Aprovada na rodada 4 (nota 8) |
| Revisão da experiência | Aprovada na rodada 1 (nota 8) |
| Revisão do detalhamento | Aprovada na rodada 3 (nota 8) |
| Revisão da implementação | Fases api e web revistas; branch aprovada na terceira rodada (nota 8) |
Status:
RASCUNHO→APROVADA→EM IMPLEMENTAÇÃO→CONCLUÍDA
É o item E2 do ROADMAP.md — a metade que a spec 0054 deixou de fora de propósito (seção 5 dela: "O acerto com a imobiliária … fica para a spec seguinte, por depender de o canal existir como dado"). O canal existe desde a 0054; esta spec é o controle sobre ele.
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.
1. Dor¶
Do 2º mês em diante, numa parceria em que a imobiliária recolhe, a garantidora não recebe do inquilino — recebe da imobiliária, depois, e no agregado. O produto sabe quanto deveria vir: desde a spec 0054 cada mensalidade carrega o canal AGENCY_PIX, e o resumo da competência soma o que ainda não foi pago nesse canal num indicador próprio, A repassar pela imobiliária (apps/api/app/domain/resumo_de_cobranca.py:288-289). O que o produto não sabe é o resto da conversa:
- quais inquilinos pagaram à imobiliária e quais não. Enquanto ninguém diz, a parcela vencida aparece como Aguardando a imobiliária — situação derivada, não gravada (
apps/api/app/schemas/billing.py:240) —, e a rotina que carimba atraso a pula de propósito (apps/api/app/services/billing_overdue_service.py:9-14), porque marcá-la afirmaria inadimplência de quem talvez tenha pagado em dia. A consequência é a cegueira que a própria 0054 registrou na jornada dela: o inquilino que pagou à imobiliária e o que não pagou ninguém são a mesma linha, e quem sabe a diferença é a parceira; - quanto a imobiliária deve no mês, por imobiliária. O indicador soma o tenant inteiro. O resumo filtra por competência, busca, situação, tipo e canal (
apps/api/app/services/billing_service.py:475-483) — não por parceira. Com duas imobiliárias recolhendo, "quanto a Solar me deve de setembro" não tem resposta no produto; - o que já chegou do repasse, e se fecha com o devido. O dinheiro chega por PIX — às vezes o total do mês de uma vez, às vezes em várias transferências (relato do usuário, seção 3, passo 5). Dar baixa parcela a parcela é possível (
BillingPayRequest.payment_channelaceitaAGENCY_PIX,apps/api/app/schemas/billing.py:88-96), mas nada liga essas baixas ao repasse que as pagou, nem diz se a soma do que chegou bate com a soma do que a parceira declarou; - que o mês daquela parceira está encerrado. Não há registro de competência fechada por imobiliária: uma baixa ou um estorno feitos depois mudam silenciosamente um mês que já foi conferido e acertado;
- a nota fiscal daquele acerto. O fechamento descrito no
ROADMAP.md:204-206termina numa NF, emitida fora do sistema; o produto não tem onde guardá-la — oDocumentpertence a renter, property, credit_analysis, guarantee ou billing (CLAUDE.md, Domínio), e um acerto mensal não é nenhum deles.
O custo de não fazer é que o produto não substitui a planilha justamente na parte do mês que envolve dinheiro de terceiro. A garantidora continua mantendo, por parceira e por mês, uma aba copiada à mão — e o único lugar em que a pergunta "este inquilino pagou?" tem resposta é uma mensagem de WhatsApp. É também o que segura o E3: o sinistro começa quando o inquilino não paga, e enquanto o não pagamento no canal da imobiliária for indistinguível do pagamento não repassado, o produto não tem como saber que um sinistro começou.
2. Quem sente¶
- A garantidora — quem dá baixa (
ADMIN_TENANT, hoje o único papel combillings:update,apps/api/app/db/rbac_seed.py:280;ANALYST,OPERATOReVIEWERleem Cobranças, combillings:list_all, mas não registram pagamento). Sente todo mês, para toda parceira que recolhe: é caminho obrigatório, não exceção — comPartnerAgency.collects_monthlyligado (apps/api/app/models/partner_agency.py:48), toda mensalidade que não sai na entrada passa por ali — da segunda em diante quando a entrada leva a 1ª mensalidade, e desde a primeira quando não leva (apps/api/app/services/guarantee_service.py:774-775). - A imobiliária — o responsável (
AGENCY_MANAGER, spec 0051). É quem hoje monta a lista de quem pagou e manda por fora. Sente no mesmo ritmo, do outro lado: não vê no produto o que a garantidora espera dela no mês. Ela lê as cobranças dentro de cada garantia (billings:list/billings:read), mas não tem a visão agregada (billings:list_allestá fora do papel dela de propósito,apps/api/app/db/rbac_seed.py:423-428) — para saber quanto deve no mês, soma garantia por garantia. - O usuário comum da imobiliária (
AGENCY_USER) é afetado só como leitor.
Volume. O produto não tem cliente em produção (CLAUDE.md, Estágio do produto), então não há operação dentro dele para medir. O que existe é a operação real do piloto, registrada na planilha docs/financeiro/Relatório Cobrança e Pagamento_Solar.xlsx: uma parceira (a Solar), com contratos ativos indo de 1 (fevereiro/2026, o início da parceria) a 36 (julho/2026) — 13 em março e abril, 19 em maio, 26 em junho (célula C11 de cada aba, a contagem da própria planilha: é COUNTA de nomes, e um mesmo inquilino pode aparecer em mais de uma linha) —, e um acerto mensal da ordem de R$ 3,7 mil a R$ 4,6 mil nos meses em que ele foi preenchido (maio e junho — células T8/T13). O alcance se sustenta pelo desenho, não pelo volume: a regra vale para toda parceira com collects_monthly, e cada parceira nova é uma aba nova.
3. A jornada de hoje¶
O caminho, como acontece no piloto, a cada competência. Duas fontes, e cada passo diz de qual vem: a planilha docs/financeiro/Relatório Cobrança e Pagamento_Solar.xlsx ([planilha]), que registra o que se calcula; e o relato do usuário na entrevista desta spec, em 2026-10-01 ([relato]), que diz como a informação e o dinheiro chegam — a planilha registra por qual canal cada valor foi pago (Boleto TXC ou Solar), mas nada sobre como a lista de quem pagou chega à garantidora, nem em quantas transferências o repasse vem.
- A imobiliária manda a lista [relato]. No fim do mês, a Solar envia à garantidora, por mensagem, quais inquilinos pagaram a mensalidade da garantia junto com o aluguel, e quanto. Fora do sistema.
- A garantidora confere contra o previsto [planilha]. Abre a aba do mês (uma por competência — de "FEVEREIRO 2026 SOLAR" a "JULHO 2026 SOLAR", e de agosto em diante só "AGOSTO 2026" …; o piloto tem uma parceira só), em que as linhas por inquilino trazem, na coluna Solar — Fiança (
G), o valor previsto, e as colunas de pagamento (M) e data (O). Nas linhas da Solar,Mnão registra pagamento: é fórmula que copiaG(M20 = G20, eM21:M33em maio,M21:M38em junho, como fórmula compartilhada=G21), eOfica vazia — em maio sóO34, uma linha de boleto, tem data. Em julhoMvirou o literal0. Quem pagou à Solar, a planilha não registra; isso está só na mensagem do passo 1. Fora do sistema. No produto ela pode ver o mesmo previsto na tela de Cobranças, filtrando o canal PIX Imobiliária — mas sem recorte por parceira, então com mais de uma imobiliária a lista mistura todas. - Lança a multa, quando houve [planilha]. A multa de quem pagou atrasado vai na coluna
Nda linha do inquilino e é somada em Pagamentos efetuados, linha MULTA – ATRASO (N11 = SUM(N20:N…)) — uma linha só para os dois canais: em abril, os 66,18 são de uma parcela recolhida pela Solar (N22, parcela 2/12,M22 = 550); em maio, os 7,32 são de um boleto da própria garantidora (N38, parcela 1/12, paga emK38/L38,M38 = 0). Se a multa recolhida pela imobiliária entrava ou não no acerto, a planilha não mostra: o único mês com multa nesse canal é abril, que ainda não tinha bloco de acerto. Fora do sistema. - Soma o acerto [planilha]. O bloco ACERTO SOLAR (
R7:T13) totaliza o que a parceira deve:T13 = SUM(T8:T12). Nele há a fiança (T8), uma linha TAXA SETUP (T9) e duas linhas sem rótulo, zeradas em todos os meses. O bloco não tem a mesma definição em todo mês, em dois pontos: - a fiança — em maio
T8 = SUM(G…), a soma do previsto; em junhoT8 = SUM(M…), a soma do pago. Dão o mesmo número (3.744,60 em maio, 4.619,95 em junho) por construção:Mé cópia deG(passo 2); - o setup — em maio
T9 = H8, eH8é o setup previsto menos o pago por boleto (SUM(E…) − SUM(K…)): a planilha cobrava da parceira o setup não pago. Em junho e julhoT9é o literal0, mesmo comH8 = 300em junho.
O que a parceira deve, e se setup entra, a planilha não decide; as duas respostas estão na seção 4. Fora do sistema.
- Recebe o dinheiro [relato]. O repasse chega por PIX, de uma vez ou em várias transferências — não há regra fixa. A garantidora confere no extrato bancário se a soma bate com o acerto. Fora do sistema.
- Dá baixa — ou não. Pode dar baixa no produto em cada parcela, uma a uma, informando o canal PIX Imobiliária no diálogo de pagamento. Nada liga essas baixas ao PIX que as pagou nem ao acerto daquele mês; e se não der, a parcela segue Aguardando a imobiliária para sempre.
- A nota fiscal — o
ROADMAP.md:204-206descreve o fechamento terminando com a garantidora emitindo nota fiscal. A planilha não registra nota nenhuma, e o usuário, perguntado nesta entrevista sobre quem emite contra quem, respondeu que ainda não sabe — então a direção que o ROADMAP descreve fica reaberta de propósito (seção 5). O que se sabe é que, quando há nota, ela é emitida e guardada fora do sistema.
Onde quebra:
- No passo 1, a informação nasce fora e nunca entra. Quem pagou é a informação que decide se uma parcela é dinheiro a caminho ou inadimplência — e ela mora numa mensagem. Tudo o que o produto mostra depois disso, inclusive o indicador de inadimplência da home, está cego para o canal da imobiliária.
- No passo 2, o produto não recorta por parceira, então a conferência é na planilha mesmo quando o dado está no banco.
- No passo 6, a baixa no produto é opcional e solta. Como ela não está amarrada a nada, o resultado depende de alguém lembrar: as duas versões do mesmo mês — planilha e produto — divergem sem que nenhuma das duas saiba.
- Depois do passo 7, nada impede o mês de mudar. Uma baixa feita semanas depois, ou estornada, altera uma competência que já foi acertada e teve nota emitida.
O que já se tentou. A spec 0054 separou o canal e criou o indicador A repassar pela imobiliária, e excluiu o AGENCY_PIX da rotina de atraso — decisões que resolveram a leitura do total, e explicitamente deixaram o controle para esta spec (seção 5 da 0054). A planilha é o contorno em uso. Nenhum campo do produto é hoje usado para outra coisa para cobrir o acerto.
4. Resultado esperado¶
Passa a ter resposta, dentro do produto e por imobiliária:
- Quais inquilinos pagaram a mensalidade à parceira neste mês, e quais não? — e quem não pagou deixa de ser Aguardando a imobiliária e passa a ser atraso do inquilino, que é o que de fato é.
- Quanto essa parceira me deve desta competência? — e o que ela deve passa a ter uma definição só, decidida pelo usuário na entrevista (2026-10-01), nos dois pontos em que a planilha oscila ou cala (seção 3, passos 3 e 4):
- deve o que declarou ter recebido, não o previsto. A mensalidade que o inquilino não pagou à imobiliária sai do acerto e passa a ser atraso do inquilino;
- o setup não entra no acerto. Ele é sempre cobrado por boleto da garantidora; o que não foi pago é atraso do inquilino, não dívida da parceira. A linha TAXA SETUP do bloco é resto de modelo, nas palavras do usuário;
- a multa que a imobiliária cobrou do inquilino atrasado entra no repasse. A planilha não decide isso (seção 3, passo 3) — decide o usuário. E a multa segue o canal da parcela: a de boleto da garantidora já está na conta dela e não é assunto do acerto; só a cobrada pela imobiliária entra. É a distinção que a linha única MULTA – ATRASO da planilha apaga. O
ROADMAP.md:202diz que a parceira repassa "só a Taxa TXC", em contraste com o aluguel; sobre que base a multa incide, a planilha não diz, e esta spec não presume. - Quanto já chegou, em quantas transferências, e falta quanto?
- Este mês desta parceira está fechado? Qual NF se refere a ele?
Como se sabe que resolveu — sinais verificáveis:
- A aba mensal da Solar deixa de ser necessária: os blocos Previsão de faturamento, Pagamentos efetuados e Acerto Solar de uma competência saem do produto para aquela parceira. A referência para conferir é junho/2026 — o acerto soma o pago (
T8 = SUM(M…)), não há multa, e a linha de setup já está zerada. Como aliMé cópia do previsto, bater significa, na prática, que no produto a parceira declare todas as mensalidades de junho como pagas pelo valor previsto. - A lista de quem pagou chega pelo produto, não pelo WhatsApp: a própria imobiliária a informa, ou a garantidora a registra a partir do que recebeu — e em qualquer dos casos ela fica registrada com quem informou.
- Nenhuma mensalidade de canal imobiliária fica Aguardando a imobiliária depois que o mês daquela parceira foi fechado.
- Uma competência fechada não muda sem que alguém a reabra, e a reabertura fica registrada.
5. Fora de escopo¶
- O atraso da imobiliária no repasse — a parceira que recebeu do inquilino e não repassou no prazo. Pedido do usuário: só o caminho feliz por ora. Sem prazo de repasse, sem cobrança da parceira, sem encargo sobre ela. O que entra é registrar o que chegou e quanto falta; o que acontece quando "falta" vira atraso fica para depois.
- A emissão da nota fiscal. Fica só o registro — número e arquivo da NF emitida fora. Integrar com emissor ou prefeitura é integração externa do porte do E4, decidida pelo usuário. Quem emite contra quem ainda não está decidido (garantidora → imobiliária, ou o contrário) — é decisão em aberto, e esta spec não depende dela para guardar o documento.
- O sinistro. O inquilino que a imobiliária declara que não pagou passa a ser inadimplente aqui — e é aí que esta spec para. Comunicado de inadimplência, prazo de 60 dias, validação e pagamento são o E3.
- Conciliação bancária automática. O PIX recebido é registrado por quem o viu no extrato; ler extrato ou integrar com banco não entra.
- As parcelas de canal da garantidora (
GUARANTOR_BOLETO,GUARANTOR_PIX). Elas aparecem na visão do mês da parceira — é como a planilha as mostra —, mas o acerto soma só o que a imobiliária recolheu. Elas continuam sendo baixadas como hoje. - Comissão ou remuneração da imobiliária pela cobrança. A planilha não mostra desconto nenhum no acerto (o total de maio é exatamente o previsto da fiança,
T13 = N10) e o contrato real não foi lido como prevendo um; se existir, é decisão nova.
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 | 0 | 3 | 4 | 7 | Reprovada — multa e definição do acerto lidas da planilha com mais firmeza do que ela sustenta; passos sem fonte declarada |
| 2 | 0 | 1 | 2 | 7 | Reprovada — a multa de maio era de boleto da garantidora, não do canal da imobiliária; a premissa "a planilha deixa a multa fora do acerto" não tinha lastro |
| 3 | 0 | 3 | 1 | 7 | Reprovada — o bloco de acerto também tinha linha de setup; "pago presumido igual ao previsto" e "nada sobre canal" afirmavam mais que a planilha. Setup levado ao usuário |
| 4 | 0 | 1 | 1 | 8 | Aprovada — o Important (coluna M é fórmula que copia G, não registro de pagamento) foi corrigido no texto antes de seguir |
Resultado: aprovada na rodada 4, nota 8.
Pendências aceitas: nenhuma — o Important e o Minor da rodada 4 foram corrigidos no texto (passo 2, passo 4, sinal de junho na seção 4, e a frase da comissão na seção 5), conferidos na planilha (M21 com <f t="shared" ref="M21:M33">G21</f> em maio).
Parte II — A experiência¶
Não preencher antes da revisão da seção 6 estar aprovada.
7. Jornada pretendida¶
O acerto de uma imobiliária numa competência passa a ser uma coisa que existe no produto, com três momentos: aberto (a parceira está declarando quem pagou), pronto para fechar (tudo declarado e o dinheiro chegou) e fechado. Os dois lados olham o mesmo acerto: a garantidora pela tela de Cobranças, a imobiliária pela tela Minha Imobiliária.
Decisões do usuário que dão forma à jornada (entrevista de 2026-10-01, além das da seção 4):
- quem declara quem pagou: a imobiliária ou a garantidora — qualquer das duas, e fica registrado quem declarou cada linha. Na imobiliária, só o responsável declara; o usuário comum da imobiliária só consulta;
- o fechamento exige toda mensalidade do canal da parceira declarada (pagou ou não pagou) e o recebido em transferências igual ao devido. Faltando dinheiro, o acerto fica aberto — atraso da parceira está fora de escopo (seção 5);
- é no fechamento que a declaração produz efeito: as declaradas pagou recebem baixa e as declaradas não pagou passam a ser atraso do inquilino. Até fechar, toda declaração se corrige;
- a nota fiscal não é exigida para fechar: anexa-se depois, a qualquer momento, e a lista mostra quais acertos fechados estão sem nota;
- há um atalho em lote: marcar todas as mensalidades ainda sem declaração como pagas, pelo valor previsto, na data de vencimento, sem multa — e corrigir só a exceção.
Ponto de entrada.
- Garantidora: menu Cobranças → nova aba Repasses, ao lado de Cobranças do mês e Atrasados. A aba usa a mesma competência já escolhida na tela — trocar de aba não troca de mês. De uma linha da aba, abre-se o acerto daquela imobiliária.
- Imobiliária: menu Minha Imobiliária (spec 0051), que ganha duas abas: Dados (o que a tela mostra hoje, sem mudança) e Repasses. Convidar operador continua no cabeçalho nas duas abas: ele é da página, não da aba. A aba lista as competências da própria parceira; de uma linha, abre-se o acerto.
Jornada, caso feliz — uma competência da Solar:
- A competência vira. O acerto de setembro da Solar já existe, Aberto, com as mensalidades de canal imobiliária daquele mês listadas e nenhuma declarada. Ninguém o cria: ele aparece porque há mensalidades da Solar no canal dela naquela competência.
- O responsável da Solar declara. Em Minha Imobiliária → Repasses, abre setembro, vê as 26 mensalidades, toca Marcar restantes como pagas. Todas passam a Pagou, com o valor previsto e a data de vencimento. Ele corrige as exceções: na linha do inquilino que pagou atrasado, muda a data e informa a multa; na do que não pagou, marca Não pagou. O acerto mostra, no topo, Devido: a soma das Pagou mais as multas.
- A Solar transfere o dinheiro, fora do sistema, de uma vez ou em partes.
- A garantidora registra o que chegou. Em Cobranças → Repasses, setembro, a linha da Solar mostra Devido R$ 6.300,00 · Recebido R$ 0,00. Ela abre o acerto, toca Registrar transferência, informa data e valor (o valor vem preenchido com o que falta); se chegou em duas partes, registra duas. Falta vai a zero.
- A garantidora fecha. Com tudo declarado e Falta zero, o botão Fechar competência se habilita. Ela confirma num diálogo que diz o efeito em números: "24 mensalidades recebem baixa · 2 passam a atraso do inquilino". O acerto vira Fechado em 05/10 por Fulana; as 24 aparecem pagas em Cobranças, canal PIX Imobiliária; as 2 aparecem em Atrasados como em atraso, não mais Aguardando a imobiliária.
- A nota fiscal, quando existir, é anexada ao acerto fechado — Anexar nota fiscal: número e arquivo. Até lá, a linha mostra Sem nota.
- Se for preciso corrigir depois, a garantidora reabre o acerto, informando o motivo. Reaberto, ele volta a Aberto, as baixas e os atrasos que o fechamento produziu são desfeitos, e a reabertura fica no histórico do acerto com quem, quando e por quê.
O mesmo vale com a garantidora declarando no passo 2, a partir da lista que recebeu por mensagem — a tela é a mesma e o atalho em lote também.
Custo de uso. O sistema já sabe e não pergunta: quais mensalidades são da parceira naquela competência, inquilino, garantia, parcela (5/12), vencimento e valor previsto. O que se digita:
- declarar quem pagou: no caso comum, um toque (o lote) e as exceções. Por exceção: Não pagou é um toque; pagou atrasado é a data e a multa. A multa vem sugerida, calculada pela regra de encargo da garantidora a partir da data informada — a mesma conta do diálogo de registrar pagamento de hoje —, e a pessoa só a altera se a parceira cobrou outra coisa. Valor pago diferente do previsto não se digita: o caso feliz é pagar o previsto (seção 5);
- registrar transferência: data (vem com hoje) e valor (vem com o que falta). Dois campos, normalmente zero digitação;
- fechar: um toque e a confirmação;
- nota fiscal: número e o arquivo;
- reabrir: o motivo, obrigatório.
7.1 Desktop¶
Garantidora — aba Repasses em Cobranças. No topo, a barra de filtros da tela continua com a competência; Tipo, Canal e Situação não se aplicam a esta aba e somem dela. Os indicadores e o resumo por tipo × canal da aba Cobranças do mês não aparecem aqui, e a ação Exportar do cabeçalho some nesta aba — exportar o acerto não entra nesta spec. Abaixo, uma tabela, uma linha por imobiliária que tenha mensalidade de canal imobiliária na competência:
| Imobiliária | Situação | Mensalidades | Devido | Recebido | Falta | Nota fiscal |
|---|---|---|---|---|---|---|
| Solar Imóveis | Aberto · 24 de 26 declaradas | 26 | R$ 6.300,00 | R$ 4.000,00 | R$ 2.300,00 | — |
- Situação é um selo: Aberto (com "N de M declaradas" ao lado), Pronto para fechar ou Fechado;
- Devido é a soma do que foi declarado pago mais as multas; enquanto há linha sem declaração, o valor vem seguido de "(parcial)", e o selo diz "N de M declaradas";
- Nota fiscal: o número, ou Sem nota em acerto fechado, ou "—" em acerto aberto;
- a linha inteira abre o acerto. Não há ação de linha além disso: as ações vivem dentro do acerto.
Uma imobiliária que não recolhe (cláusula da parceria) não aparece: ela não tem mensalidade nesse canal. Cabem, sem rolar, dezenas de linhas — o número de parceiras de uma garantidora —, então não há paginação; há a busca por nome da imobiliária.
O acerto (garantidora e imobiliária, mesma tela). Página própria, com o cabeçalho padrão de página de detalhe: título Solar Imóveis — setembro/2026 e, à direita, as ações do momento (ver abaixo). Logo abaixo do cabeçalho, a faixa de status, como na tela de garantia: o selo de situação e, quando Fechar competência está desabilitado, o motivo por extenso. Logo abaixo, três painéis lado a lado, que são os três blocos da planilha:
- Previsão — o que a competência prevê para as garantias da parceira, por linha de tipo e canal: Setup — Boleto, Fiança — Boleto, Fiança — PIX Imobiliária, e o total;
- Pagamentos — o que já se sabe pago, nas mesmas linhas, mais Multa — PIX Imobiliária; as linhas de boleto vêm das baixas que a garantidora já registra hoje, as de imobiliária vêm das declarações;
- Acerto — Devido, Recebido, Falta, e a lista curta das transferências (data e valor), com o botão Registrar transferência no pé do painel. Em acerto aberto, cada transferência tem, para a garantidora, o ícone Remover (44px), com confirmação — é a saída para a transferência registrada errado (decisão do usuário, 2026-10-02).
Abaixo dos painéis, a lista das mensalidades da parceira na competência, todas as que as garantias dela têm — dos dois canais, como na planilha —, com as de boleto da garantidora esmaecidas e sem ação, e um aviso curto acima: "As cobranças por boleto aparecem para conferência; só as de PIX Imobiliária entram no acerto." Colunas:
| Inquilino | Garantia | Parcela | Canal | Vencimento | Previsto | Declaração | Pago em | Multa | Declarado por |
|---|---|---|---|---|---|---|---|---|---|
- Declaração: Pagou, Não pagou ou Sem declaração;
- em acerto aberto, para quem pode declarar, a coluna Declaração é um par de botões (Pagou / Não pagou), e marcar Pagou abre na própria linha os campos Pago em (vem com o vencimento) e Multa (vem com a sugestão, zero se pago até o vencimento). A linha salva sozinha ao sair do campo, com um indicador discreto de Salvo;
- acima da lista, à direita, Marcar restantes como pagas — só aparece se houver linha sem declaração e quem vê pode declarar. Pede confirmação com o efeito em número ("Marcar 24 mensalidades como pagas no vencimento, sem multa?"), porque grava muitas linhas de uma vez. É tudo ou nada: se falhar, nenhuma linha muda, e a mensagem diz "Não foi possível marcar. Nenhuma mensalidade foi alterada." com Tentar de novo;
- a lista segue a paginação padrão das tabelas do app.
Ações no cabeçalho do acerto, por momento:
(Registrar transferência não é ação de cabeçalho: vive no pé do painel Acerto, para a garantidora, enquanto o acerto está aberto.)
- Aberto, para a garantidora: Fechar competência, desabilitado, com o motivo no tooltip: "Faltam 2 mensalidades a declarar" ou "Faltam R$ 2.300,00 a receber" — e o mesmo texto, por extenso, na faixa de status, que é como ele chega ao mobile, onde não há passar o mouse;
- Pronto para fechar, para a garantidora: Fechar competência habilitado;
- Fechado, para a garantidora: Anexar nota fiscal (ou Ver nota fiscal, se já anexada) e Reabrir, este como ação secundária;
- para a imobiliária, nenhuma ação no cabeçalho: ela declara na lista e vê o resto.
Acerto fechado com mensalidade que entrou depois (uma garantia emitida com início retroativo, ou uma cobrança restaurada): logo abaixo da faixa de status, um aviso "2 mensalidades entraram depois do fechamento. Reabra o acerto para declará-las."; as linhas aparecem na lista com o selo Entrou depois, sem botões. Decisão do usuário (2026-10-02): elas entram e pedem reabertura.
Abaixo de tudo, o histórico do acerto, na linha do tempo padrão: transferências registradas, fechamento, reabertura com motivo, nota anexada — quem e quando. As declarações não entram no histórico: são dezenas por mês, e afogariam os cinco eventos que importam. Quem declarou cada linha está na própria linha — a coluna Declarado por no desktop, e no mobile uma linha de contexto do cartão (ver 7.2). Corrigir uma declaração substitui o autor pelo de quem corrigiu.
Imobiliária — aba Repasses em Minha Imobiliária. Tabela de competências da parceira, mais recente primeiro: Competência, Situação, Mensalidades, Devido, Recebido, Falta, Nota fiscal. A linha abre o acerto, que é a mesma tela descrita acima com o que a imobiliária pode fazer.
7.2 Mobile (375px)¶
Quem usa no celular, e para quê (decisão do usuário): consultar sim, operar pouco. O responsável da imobiliária declara quem pagou pelo celular; registrar transferência, fechar, anexar nota e reabrir são trabalho de mesa, mas funcionam em 375px.
Garantidora — aba Repasses. As abas da tela de Cobranças já rolam na horizontal no mobile; Repasses entra como a terceira. A competência fica na barra de filtros compacta do mobile, como já fica em Cobranças do mês, com a busca por imobiliária. Cada imobiliária vira um cartão:
- título: nome da imobiliária; selo de situação no canto;
- linha de contexto: "26 mensalidades · 24 declaradas";
- linha de contexto: "Recebido R$ 4.000,00" e, se fechado, "NF 1234" ou "Sem nota";
- valor em destaque: Falta R$ 2.300,00, ou Fechado quando fechado;
- tocar no cartão abre o acerto.
O acerto no celular. O cabeçalho é o cabeçalho padrão de página de detalhe, o mesmo da tela de garantia no mobile: título e as ações do momento, com o selo na faixa de status logo abaixo — no máximo duas em cada momento (Fechar competência no aberto; Anexar nota fiscal e Reabrir no fechado), o que cabe nos espaços que ele já tem. Registrar transferência não está no cabeçalho em nenhum viewport: fica no pé do painel Acerto, junto da lista de transferências. Os três painéis empilham, e só o Acerto vem aberto; Previsão e Pagamentos vêm recolhidos, com o total no título ("Previsão · R$ 6.629,49"), porque no celular a pergunta é quanto falta, não a composição.
A lista de mensalidades vira cartões, e só as de canal imobiliária aparecem no celular: as de boleto, que na mesa são só conferência, sairiam como cartões sem ação ocupando a rolagem. Um aviso no topo da lista diz "Mostrando as cobranças de PIX Imobiliária". Cada cartão:
- título: inquilino; selo da declaração no canto;
- linha de contexto: "5/12 · vence 10/09";
- linha de contexto, quando declarada: "Declarado por Solar · Fulano" ou "Declarado pela garantidora · Beltrana";
- valor: o previsto; se pago com multa, a multa ao lado ("+ R$ 12,30");
- em acerto aberto, para quem declara, dois botões de 44px no pé do cartão — Pagou e Não pagou. Tocar Pagou marca pelo previsto, na data de vencimento, sem multa — o caso comum, sem teclado. Para o caso atrasado, tocar no corpo do cartão abre um diálogo em tela cheia — o mesmo comportamento do diálogo de registrar pagamento no mobile — com Pago em (seletor de data) e Multa (já sugerida). São dois campos no topo da tela; o teclado numérico, ao subir, não cobre nenhum deles, e o botão Confirmar fica preso ao rodapé, acima do teclado — como no diálogo de registrar pagamento.
Marcar restantes como pagas fica no topo da lista, largura cheia, acima dos cartões. A lista pagina com a barra de paginação fixa no rodapé, a mesma das outras listas do app no mobile.
Registrar transferência e Anexar nota fiscal abrem em tela cheia, como o diálogo de registrar pagamento já faz no mobile. A transferência tem dois campos (data e valor), ambos preenchidos. A nota tem número e o seletor de arquivo do aparelho.
O que não aparece no celular: as linhas de boleto da lista de mensalidades (ver acima). A coluna Declarado por não some: vira a linha de contexto do cartão.
Imobiliária — Minha Imobiliária no celular. As duas abas (Dados, Repasses) no topo, roláveis. Em Repasses, cada competência vira um cartão: título setembro/2026, selo de situação, linha "24 de 26 declaradas", valor Devido R$ 6.300,00. Tocar abre o acerto como descrito acima.
PWA. Nada de offline: declarar e registrar exigem rede, como o resto do app. A tela entra no app instalado como qualquer outra, pela navegação de sempre.
7.3 Estados¶
- Carregando. Aba Repasses e lista de competências: esqueleto de linhas no desktop, esqueleto de cartões no mobile. O acerto: os três painéis em esqueleto e a lista abaixo.
- Vazio — nenhuma imobiliária recolhe nesta competência (garantidora): "Nenhuma imobiliária recolhe mensalidades nesta competência. Quem recolhe é definido no cadastro de cada imobiliária parceira." Para quem pode editar imobiliárias, um botão leva a Imobiliárias; para quem não pode, só o texto.
- Vazio — a busca não achou (garantidora): "Nenhuma imobiliária com esse nome nesta competência" e o botão Limpar busca, como as outras listas.
- Vazio — a imobiliária não tem acerto nenhum (Minha Imobiliária → Repasses): "Ainda não há repasses. Eles aparecem aqui a partir da primeira competência com mensalidades que vocês recolhem." Uma imobiliária que não recolhe vê exatamente isto, sem que a tela diga a ela qual é a cláusula da parceria.
- Erro ao carregar (lista ou acerto): alerta com o motivo e Tentar de novo; os painéis de totais somem quando a lista falhou — total ao lado de lista que não carregou é pior que nenhum, como já é na aba Cobranças do mês.
- Erro ao declarar uma linha: a linha volta ao estado anterior e mostra, nela mesma, "Não foi possível salvar. Tentar de novo"; as outras linhas seguem utilizáveis.
- Acerto fechado enquanto alguém declarava: a linha que tentou salvar recebe "Este mês já foi fechado pela garantidora. Peça a reabertura para corrigir.", e a tela recarrega como Fechado, sem os botões de declaração.
- Correção que deixaria o devido abaixo do recebido: a linha volta ao estado anterior e mostra "Com esta correção o devido ficaria R$ X abaixo do que já foi recebido. Peça à garantidora que remova a transferência registrada a mais." — para a garantidora, o texto termina em "Remova a transferência registrada a mais.".
- Recebido a mais (uma mensalidade declarada paga foi removida ou cancelada depois de o dinheiro ser registrado): o painel Acerto mostra Falta como Recebido a mais R$ X, e a faixa de status diz "O recebido passou do devido em R$ X. Remova a transferência registrada a mais para fechar."; Fechar competência fica desabilitado.
- Declaração que ficou velha no fechamento (os Parâmetros de multa mudaram depois dela): o diálogo de fechar não fecha, diz "N declarações precisam ser refeitas" — a multa passou do teto atual, ou o vencimento de uma não pagou foi mudado para o futuro —, e as linhas ficam marcadas na lista até serem declaradas de novo.
- Transferência que passa do devido: o diálogo não salva e diz "O valor passa R$ X do que falta. Confira a transferência ou as declarações." Registrar a mais não é caso feliz, e aceitar produziria um Falta negativo que nada explica.
- Fechar sem condição (corrida: alguém desmarcou uma linha entre a tela carregar e o clique): o diálogo de confirmação não fecha o mês e mostra o que falta, com o acerto recarregado.
- Falha ao fechar, reabrir, registrar transferência ou anexar nota (rede ou servidor): o diálogo não fecha, mostra o erro dentro dele e mantém o que foi digitado, para tentar de novo — a convenção de feedback do app (
docs/ui-patterns.md, Feedback). Na nota fiscal, arquivo recusado (formato ou tamanho) é dito antes do envio, no próprio campo do arquivo, com o que se aceita. - Sem permissão:
- usuário comum da imobiliária: vê Repasses e o acerto, sem os botões de declaração nem o lote; a lista mostra a declaração como texto;
- leitores da garantidora (analista, operador, consulta): veem a aba e o acerto, sem declarar, registrar, fechar, anexar ou reabrir — as ações não aparecem, em vez de aparecerem desabilitadas;
- administrador da plataforma: a aba Repasses não aparece, pelo mesmo motivo por que o resumo da competência já é negado a ele (spec 0054): somar a carteira de um cliente não é papel da plataforma;
- imobiliária tentando abrir acerto de outra parceira por link: o mesmo alerta que as páginas de detalhe mostram quando o registro não é achado ("Acerto não encontrado"), idêntico ao de um acerto que não existe — sem dizer que a outra parceira existe.
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 | 2 | 5 | 8 | Aprovada — os dois Important (botão do diálogo mobile no rodapé, não no cabeçalho; onde fica "declarado por") e os cinco Minor corrigidos no texto antes da Parte III |
Resultado: aprovada na rodada 1, nota 8.
Pendências aceitas: nenhuma — todos os achados da rodada 1 foram corrigidos no texto: confirmação do diálogo no rodapé, autor da declaração na linha (não no histórico), confirmação e falha do lote, falha genérica nos diálogos e arquivo recusado, Exportar e Convidar operador por aba, selo e motivo na faixa de status, alerta de não encontrado, e o Devido parcial.
Parte III — Detalhamento¶
9. Regras de negócio¶
O acerto.
- RN1 — O que é um acerto. Um acerto é o par (imobiliária parceira, competência), dentro de uma garantidora. Seus membros são as cobranças que satisfazem, ao mesmo tempo:
channel = AGENCY_PIX;reference_monthigual à competência; garantia compartner_agency_idigual à imobiliária; não removidas (deleted_atnulo); estatus ≠ CANCELLED. A imobiliária vem da garantia — a cobrança não tem coluna de imobiliária, e não ganha uma. - RN2 — O acerto existe sem ser criado. Ele existe quando tem ao menos um membro, ou quando já há registro gravado para ele (RN3). Nada cria acerto por agendamento nem ao virar o mês: as listas (RN20, RN21) derivam os acertos dos membros. O registro em
agency_settlementsnasce na primeira escrita sobre o acerto — declarar, declarar em lote ou registrar transferência — e a leitura nunca grava. - RN3 — O registro. Tabela
agency_settlements, uma linha por(tenant_id, partner_agency_id, reference_month), com unicidade nessa tripla. Guarda só o que não se deriva: o estado gravado (OPENouCLOSED), quem fechou e quando, e o número da nota fiscal. Toda escrita sobre o acerto começa pelo mesmo passo: obter a linha criando-a se faltar e travando-a, no padrão que o repositório já tem emapps/api/app/repositories/numbering_sequence_repo.py:30-60—begin_nested()para tentar oINSERT,IntegrityErrorcapturado dentro do savepoint (que não aborta a transação doCommitRoute), eSELECT ... with_for_update()da linha. É construção portátil, nãoON CONFLICTde dialeto. A trava serializa as escritas do acerto — declarar, lote, registrar e remover transferência, fechar, reabrir, nota. Ela não serializa as escritas do lado da cobrança (remover, restaurar, editar vencimento, encerrar a garantia, emitir), que não passam por este service; o que essas podem fazer com o acerto aberto está na RN9 (faltanegativa) e na RN12 (revalidação no fechamento), e com o fechado na RN14. - RN4 — A situação mostrada. Derivada, nunca gravada além do
OPEN/CLOSED: CLOSED— o registro dizCLOSED. Comlate_members > 0(RN14a), a resposta traz a contagem, e a tela mostra o aviso;READY(Pronto para fechar) — aberto, todo membro em aberto declarado (RN6) efalta = 0(RN9), comdevido > 0ou ao menos um membro declarado;OPEN(Aberto) — os demais; comfalta < 0(RN9), a resposta trazoverpaid = true.
A declaração.
- RN5 — Declarar é por cobrança, e fica na cobrança. Cinco colunas novas em
billings, todas anuláveis:agency_outcome(enumPAID/UNPAID),agency_paid_on(data),agency_late_fee(Numeric(12,2)),agency_declared_by(FKusers.id,RESTRICT) eagency_declared_at(timestamptz). Uma declaração nova substitui a anterior por inteiro, autor incluído. Não há "desfazer declaração" (a Parte II não o tem); corrige-se declarando o contrário. - RN5a — Corrigir não pode deixar o devido abaixo do recebido. Declaração (ou correção) que reduz o
devidoe o deixa abaixo dorecebido(RN9) — declaração que mantém ou aumenta odevidopassa sempre, mesmo comfaltajá negativa; o lote (RN8a) só aumenta, e por isso não passa por esta regra — →422 DECLARATION_BELOW_RECEIVED, comdetails = [{"field": "devido", "value": "X"}, {"field": "recebido", "value": "Y"}]. Decisão do usuário (2026-10-02): recusar, e oferecer a remoção da transferência registrada errada (RN10a). A conta é feita sob a trava da RN3. - RN6 — Quais membros se declaram. Só membros com
status ∈ {PENDING, OVERDUE}— os em aberto. Membro jáPAIDantes de entrar no acerto (baixa avulsa anterior a esta spec, RN18) não se declara, não entra no devido e aparece na lista como Baixada fora do acerto. Declarar membroPAID→422 BILLING_ALREADY_SETTLED(o código que a baixa já usa,billing_service.py:235-239). - RN7 — Validação da declaração
PAID. paid_onelate_feeobrigatórios — ausente qualquer um →422 DECLARATION_FIELDS_REQUIRED(código registrado na implementação: a regra pedia os dois e não nomeava o erro);paid_onnão futuro — futuro →422 DECLARATION_DATE_IN_FUTURE, pela mesma razão da RN17 da 0054;late_feeobrigatório,≥ 0, e não maior que o encargo cobrável da cobrança naquela data, calculado porencargo_da_cobranca(apps/api/app/domain/encargo.py:169) — o mesmo teto que a baixa avulsa aplica (RN19 da 0054,apps/api/app/domain/distribuicao_de_baixa.py:131-133). Acima →422 DECLARATION_LATE_FEE_EXCEEDS_CHARGE, com o teto nodetails. Pago até o vencimento, o teto é zero;- o valor pago é o
amountda cobrança, sempre: não há campo de valor. Pagar diferente do previsto não é caso feliz (seção 5). - RN8 — Validação da declaração
UNPAID.paid_onelate_feedevem vir ausentes ou nulos — presentes →422 DECLARATION_FIELDS_NOT_ALLOWED. E só se declara não pagou o que já venceu:due_date < hoje(data local,hoje_local()); senão422 UNPAID_NOT_YET_DUE— não pagar o que ainda não venceu não é fato. - RN8a — A declaração em lote. Marca como
PAID, compaid_on = due_dateelate_fee = 0, todo membro em aberto sem declaração; não toca membro já declarado. Membro cujodue_dateé futuro fica de fora do lote (RN7 recusaria a data futura) — o lote devolve quantos marcou e quantos pulou por vencimento futuro. É uma transação: falhou, nenhuma linha muda. - RN8b — A sugestão de multa. O endpoint que calcula o encargo numa data (
GET /guarantees/{gid}/billings/{bid}/charge?on=,routers/billing.py:485-528) exigebillings:update, que oAGENCY_MANAGERnão tem e não deve ganhar (é a permissão de dar baixa). Por isso a lista de membros (RN21b) devolve, para cada membro em aberto,late_fee_today— o encargo cobrável hoje —, e a declaração recalcula pela data informada (RN7), devolvendo o teto no erro se passar. A tela usa o mesmo caminho para os dois lados,ADMIN_TENANTinclusive: um só comportamento de sugestão, em vez de dois.
Os números.
- RN9 — As contas do acerto, derivadas a cada leitura, em
Decimal: devido= Σ(amount+agency_late_fee) dos membros declaradosPAIDe em aberto (RN6), enquanto o acerto está aberto; fechado, a conta é outra e substitui esta: Σ(amount_paid) dos membros comagency_settlement_id= o acerto (RN12). Membro tardio (RN14a), mesmo com declaração antiga, não entra nodevidodo fechado;recebido= Σamountdas transferências do acerto (RN10);falta=devido − recebido. Pode ficar negativa, e isto é estado previsto, não defeito: as escritas do acerto não a levam abaixo de zero (RN5a, RN10, sob a trava da RN3), mas as do lado da cobrança podem — remover um membro declaradoPAID(billing_service.delete,:612-639) ou encerrar a garantia, que cancela as mensalidades futuras pendentes (guarantee_service.py:681-705), tiram o membro do acerto e odevidocai depois de o dinheiro já ter sido registrado. Comfalta < 0o acerto não fecha (RN11), e a saída é remover a transferência a mais (RN10a) — a tela diz isso (Parte II, 7.3);parcial= existe membro em aberto sem declaração.- RN9a — Os três painéis (Parte II, 7.1) por linha de tipo × canal, só com cobranças das garantias da imobiliária na competência — dos três canais, como a planilha:
- Previsão: Σ
amountpor(fee_type, channel), de toda cobrança não removida e não cancelada; - Pagamentos: as de canal da garantidora, Σ do que a baixa registrou (
setup_paid,guarantee_paid,misc_paidpor balde) dasPAID; as deAGENCY_PIX, Σamountdas declaradasPAID(ou baixadas pelo fechamento), e a linha Multa — PIX Imobiliária = Σagency_late_feedas mesmas; - Acerto:
devido,recebido,falta(RN9).
A conta é pura, num módulo de domínio novo (domain/acerto_com_imobiliaria.py), no mesmo desenho de domain/resumo_de_cobranca.py: o service carrega e escopa, o domínio soma.
As transferências.
- RN10 — Registrar transferência. Tabela
agency_settlement_transfers:received_on(data, obrigatória, não futura →422 TRANSFER_DATE_IN_FUTURE),amount(Numeric(12,2),> 0),notes(texto opcional),created_by,created_at. Só em acerto aberto.amount > falta→422 TRANSFER_EXCEEDS_REMAINING, com afaltanodetails. Não há edição de transferência; um valor digitado a menos se completa registrando a diferença. As escritas do acerto não levam afaltaabaixo de zero (RN5a, e esta regra, sob a trava da RN3); as do lado da cobrança podem, e isso é estado previsto (RN9) — não háCheckConstraintnem invariante de domínio defalta ≥ 0. - RN10a — Remover transferência. Só em acerto aberto, por quem tem
agency_settlements:manage. Remoção física da linha, com auditoriaagency_settlement/TRANSFER_REMOVED(data, valor, quem registrou e quem removeu) — o histórico é a auditoria, não a tabela. Em acerto fechado →409 SETTLEMENT_CLOSED; transferência de outro acerto →404 TRANSFER_NOT_FOUND.
Fechar e reabrir.
- RN11 — Pré-condições de fechar. Acerto aberto; nenhum membro em aberto sem declaração; e
falta = 0. Faltando qualquer uma →422 SETTLEMENT_NOT_READY, comdetails = [{"field": "undeclared", "value": N}, {"field": "falta", "value": "X.YY"}]— lista, que é o tipo deAppError.details(apps/api/app/core/exceptions.py:37). Acerto sem membro declaradoPAIDe semUNPAID(só membros baixados fora) também não fecha — não há o que acertar:422 NOTHING_TO_SETTLE. - RN12 — O efeito de fechar, numa transação só:
- cada membro declarado
PAIDrecebe baixa pelo mesmo caminho da baixa avulsa — a distribuição dedistribuir(apps/api/app/domain/distribuicao_de_baixa.py:86) comamount_paid = amount + agency_late_fee,guarantee_paid = amount(o membro é semprefee_type = GUARANTEE,guarantee_service.py:786),charge_paid = agency_late_fee,waive_late_fee = False,paid_on = agency_paid_on,payment_channel = AGENCY_PIX— gravando os campos quepaygrava (billing_service.py:270-279). O passo de gravação é extraído depaypara um método privado comum, para os dois caminhos não divergirem; - cada membro declarado
UNPAIDpassa aOVERDUE; - todo membro afetado ganha
agency_settlement_id= o acerto (coluna nova, FKagency_settlements.id,RESTRICT), que é o que a reabertura usa (RN13); - o registro vai a
CLOSED, comclosed_ateclosed_by; - as pré-condições da RN11 são avaliadas depois da releitura travada, sobre o mesmo conjunto relido — e não antes. Uma cobrança removida, cancelada, restaurada ou emitida que tenha comitado entre a tela carregar e a releitura já está refletida nele: se faltar declaração ou a
faltanão for zero,422 SETTLEMENT_NOT_READYe nada é gravado; - antes de gravar, os membros são relidos com
with_for_update()— o que faz uma remoção ou edição concorrente da cobrança esperar o fechamento, e depois encontraragency_settlement_idgravado (RN14) — e cada declaração é revalidada: asPAIDpela RN7 com o encargo de agora (os Parâmetros podem ter mudado), e asUNPAIDpela RN8 com odue_datede agora (o vencimento pode ter sido editado para o futuro depois da declaração,billing_service.py:202-208). Alguma que não passe →422 DECLARATION_STALE, comdetails = [{"field": "<billing_id>", "reason": "LATE_FEE_ABOVE_MAX" | "NOT_YET_DUE" | "DATE_IN_FUTURE", "max": "X"}], e nada é gravado; a tela marca as linhas, e a pessoa as declara de novo. Assimdistribuirnunca levantaPAYMENT_EXCEEDS_DUE/PAYMENT_ALLOCATION_MISMATCHno fechamento; - auditoria: uma linha
agency_settlement/CLOSEDcom a contagem de baixados e de atrasados, mais uma linhabilling/PAID(ouOVERDUE) por membro, com o mesmopayloadque a baixa avulsa grava e"via": "agency_settlement". - RN13 — Reabrir. Só acerto
CLOSED;reasonobrigatório (não vazio depois destrip) → senão422 REOPEN_REASON_REQUIRED. Numa transação: todo membro comagency_settlement_id= este acerto volta aPENDING, comamount_paid,paid_at,paid_on,payment_channel,setup_paid,guarantee_paid,misc_paid,charge_paid,discount_amounteagency_settlement_idanulados; as declarações ficam (é para corrigi-las); transferências e nota fiscal ficam; o registro volta aOPENcomclosed_at/closed_byanulados. Auditoriaagency_settlement/REOPENEDcom o motivo e os ids devolvidos. Um membro que estavaOVERDUEantes do fechamento (marcado à mão) volta aPENDING, não aOVERDUE: a situação derivada o mostra Aguardando a imobiliária enquanto o acerto estiver aberto, e o próximo fechamento o carimba de novo se continuar não pagou. - RN14 — O acerto fechado é imutável, salvo pelo reabrir, pela nota fiscal e pela entrada tardia da RN14a:
- declarar, declarar em lote, registrar e remover transferência em acerto
CLOSED→409 SETTLEMENT_CLOSED; - as operações de cobrança existentes recusam o que o fechamento gravou:
pay,update,mark_overdue,deleteerestoresobre cobrança comagency_settlement_idnão nulo →409 BILLING_IN_CLOSED_SETTLEMENT. (agency_settlement_idsó é não nulo enquanto o acerto está fechado: o fechamento o grava, a reabertura o anula — RN12, RN13.) Esta conferência é a primeira em cada uma dessas operações, antes deBILLING_ALREADY_SETTLED,BILLING_NOT_PENDINGe da RN15, e é feita sobre a cobrança relida comwith_for_update()— é o que a faz enxergar um fechamento concorrente (RN12). - RN14a — Entrada tardia num acerto fechado. Uma cobrança pode passar a satisfazer a RN1 depois do fechamento por dois caminhos que o código tem: a emissão de garantia com início retroativo, que cria as mensalidades direto pelo repositório (
guarantee_service.py:774-793), e a restauração de um membro removido antes do fechamento (POST /billings/{id}/restore). Decisão do usuário (2026-10-02): elas entram e pedem reabertura — nenhuma das duas operações é bloqueada. A cobrança ficaPENDING, semagency_settlement_id, e é um membro tardio: o detalhe a lista comjoined_after_close = truee devolvelate_members(contagem); ela não se declara (409 SETTLEMENT_CLOSED) e não tem baixa avulsa (RN15). O único caminho é reabrir — ao reabrir, ela vira membro comum, e o próximo fechamento a exige declarada como qualquer outra (RN11). A rotina de atraso continua sem tocá-la (RN16). A cobrança avulsa e a edição de cobrança não produzem membro (a avulsa nasce sempre no canal da garantidora,billing_service.py:155-163; a edição só mudadue_dateenotes,apps/api/app/schemas/billing.py:40-42), e por isso não estão nesta regra.
A baixa avulsa e o atraso, daqui em diante.
- RN15 — Mensalidade de canal imobiliária não tem mais baixa avulsa.
POST .../billings/{id}/paysobre cobrança comchannel = AGENCY_PIX→422 AGENCY_BILLING_SETTLED_BY_AGREEMENT("Esta mensalidade é recolhida pela imobiliária. Registre o pagamento no acerto do mês."). É a consequência direta de a declaração produzir efeito só no fechamento (seção 7): com dois caminhos de baixa, o acerto e a cobrança contariam o mesmo dinheiro duas vezes. A recusa olha ochannelprevisto, não opayment_channel— boleto pago por PIX continua aceito (RN15 da 0054,test_cobranca_baixa.py). - RN16 — A rotina de atraso continua sem tocar
AGENCY_PIX(billing_overdue_service.py:9-14). Quem carimba o atraso dessas mensalidades passa a ser o fechamento (RN12).mark_overduemanual continua aceito em membro de acerto aberto. - RN17 — A nota fiscal. Número (
invoice_number, texto, 1 a 60 caracteres depois destrip, obrigatório — vazio →422 INVOICE_NUMBER_REQUIRED, acima de 60 →422 INVOICE_NUMBER_TOO_LONG—, sem formato imposto, que é de prefeitura para prefeitura) gravado no registro, e o arquivo comoDocumentcom a sexta FK mutuamente exclusiva,agency_settlement_id(documents.agency_settlement_id, FKagency_settlements.id,SET NULL, como as outras cinco,apps/api/app/models/document.py:16-35). Só em acertoCLOSED→ senão409 SETTLEMENT_NOT_CLOSED. Anexar de novo substitui: o documento anterior é removido logicamente e o número é sobrescrito. Arquivo pelas regras de upload que já existem (ALLOWED_CONTENT_TYPES,MAX_UPLOAD_SIZE_MB = 10,apps/api/app/core/config.py:129-130; a mesma validação dedocument_service.py:115-127), com os mesmos códigos de erro dela. A nota não some na reabertura. - RN18 — Mensalidades de canal imobiliária já baixadas antes desta spec ficam como estão (RN6). Não há migração de dado: o produto não tem cliente (CLAUDE.md, Estágio do produto), e no dev-remote o dado é descartável.
As leituras.
- RN19 — Competência. Toda rota recebe
reference_monthno formatoAAAA-MM, validado porvalidar_competencia(billing_service.py:57-70) →422 INVALID_REFERENCE_MONTH. - RN20 — A lista por competência (aba Repasses): uma linha por imobiliária com ao menos um membro na competência, ou com registro gravado; nome (
trade_nameouname), situação (RN4),members(contagem),declared(contagem de declarados entre os em aberto),devido,parcial,recebido,falta,invoice_number. Ordenada por nome. Filtrosearchpor nome ou nome fantasia da imobiliária, sem acento e sem caixa, com a mesma função de busca que a 0021 pôs emrepositories/busca.py. Sem paginação: o número de parceiras de uma garantidora é de dezenas (Parte II, 7.1); o endpoint devolve até 500 e diztruncated: truese houver mais — limite declarado, não silencioso. - RN20a — Imobiliária sem
collects_monthlyhoje mas com membros na competência (a cláusula mudou depois de emitida a garantia) aparece: o acerto é dos membros, não da cláusula atual. - RN21 — A lista por imobiliária (aba Repasses de Minha Imobiliária): uma linha por competência com acerto existente (RN2), da mais recente para a mais antiga, paginada (
skip/limit, padrão 12), com os mesmos campos da RN20 menos o nome. - RN21a — O detalhe: cabeçalho (imobiliária, competência, situação,
closed_at,closed_bycom nome,invoice_numbere o id do documento da nota), os três painéis (RN9a), as transferências (data, valor, observação, quem registrou) e o histórico (RN22). - RN21b — Os membros, paginados (
skip/limit, padrão 25), com filtro opcionalchannel(o mobile pede sóAGENCY_PIX, Parte II 7.2). Por linha: id da cobrança, id e número da garantia, inquilino,installment_number/total,channel,due_date,amount,status,agency_outcome,agency_paid_on,agency_late_fee, nome de quem declarou e se é da garantidora ou da imobiliária,late_fee_today(RN8b) esettled_outside(RN6). Ordenada por inquilino. Inclui as cobranças de canal da garantidora das garantias da imobiliária na competência (só leitura). - RN22 — O histórico do acerto é a auditoria filtrada por
entity_type = "agency_settlement"eentity_id= o registro:TRANSFER_REGISTERED,TRANSFER_REMOVED,CLOSED,REOPENED,INVOICE_ATTACHED. Declarações não são auditadas como evento do acerto (Parte II, 7.1): o autor fica na própria cobrança (RN5), e o efeito delas é auditado no fechamento (RN12). Acerto sem registro gravado tem histórico vazio.
10. Autorização e isolamento¶
10.1 Matriz de papéis¶
| Papel | Pode | Não pode |
|---|---|---|
ADMIN_GLOBAL | Nada desta spec | Ler qualquer lista, detalhe ou membro de acerto (403 PLATFORM_ADMIN_CANNOT_READ_SETTLEMENTS, guarda de código, RN23); declarar; registrar transferência; fechar; reabrir; anexar ou baixar nota de acerto |
ADMIN_TENANT | Ler as listas, o detalhe e os membros de qualquer imobiliária do próprio tenant; declarar; declarar em lote; registrar e remover transferência; fechar; reabrir; anexar nota | Alcançar acerto de outro tenant (404); fazer baixa avulsa em mensalidade AGENCY_PIX (RN15) |
ANALYST, OPERATOR, VIEWER | Ler as listas, o detalhe, os membros e a nota de qualquer imobiliária do próprio tenant | Declarar, declarar em lote, registrar ou remover transferência, fechar, reabrir, anexar nota (403 — no próprio tenant); alterar ou remover a nota pela rota genérica de documentos, embora o OPERATOR tenha documents:update/documents:delete (403, RN24) |
AGENCY_MANAGER | Ler a lista por imobiliária, o detalhe, os membros e a nota da própria imobiliária; declarar e declarar em lote no acerto da própria imobiliária | Ler a lista por competência de outras parceiras (só vê a própria linha); alcançar acerto de outra imobiliária (404); registrar ou remover transferência, fechar, reabrir, anexar nota (403) |
AGENCY_USER | Ler a lista por imobiliária, o detalhe, os membros e a nota da própria imobiliária | Declarar e declarar em lote (403); tudo o que o AGENCY_MANAGER também não pode |
Parceiro sem vínculo (papel de canal, partner_agency_id nulo — RN8a da 0051): escopo impossível de agency_scope (apps/api/app/services/scoping.py:71-81), então toda leitura dá lista vazia ou 404, e toda escrita dá 403 AGENCY_LINK_REQUIRED (exigir_vinculo, scoping.py:102-121).
Lastro de cada linha, para não haver política inventada:
- escrever sobre dinheiro da garantidora é só do
ADMIN_TENANT— hoje ele é o único combillings:update(rbac_seed.py:280), e registrar transferência, fechar (que dá baixa), reabrir (que desfaz baixa) e anexar nota são escrita financeira do mesmo tipo; - ler é de quem já lê a tela agregada de Cobranças (
billings:list_allemANALYST,OPERATOR,VIEWER), onde a aba vive; - declarar do lado da imobiliária é só do
AGENCY_MANAGER, e da garantidora só doADMIN_TENANT— decisão do usuário (entrevista de 2026-10-01; seção 7); - o
ADMIN_GLOBALfora de tudo segue a RN26a da 0054 (_garantir_que_pode_agregar,billing_service.py:455-473): o acerto é soma da carteira de um cliente por parceira.
10.2 Permissões¶
Recurso novo agency_settlements, três ações, no _PERMISSIONS do rbac_seed.py:
| Permissão | Descrição | Papéis |
|---|---|---|
agency_settlements:read | Ver acertos com imobiliárias | ADMIN_TENANT, ANALYST, OPERATOR, VIEWER, AGENCY_USER (e, pela união, AGENCY_MANAGER) |
agency_settlements:declare | Declarar quais inquilinos pagaram à imobiliária | ADMIN_TENANT, AGENCY_MANAGER |
agency_settlements:manage | Registrar e remover transferência, fechar, reabrir e anexar nota fiscal do acerto | ADMIN_TENANT |
AGENCY_MANAGERganhaagency_settlements:declareno conjunto que soma aoAGENCY_USER(rbac_seed.py:444-446), não no doAGENCY_USER.- As três entram na subtração do
ADMIN_GLOBAL(rbac_seed.py:230-248). Como lá se registra, a fixture de teste dele concede tudo sem consultar o mapa — por isso a recusa de fato é a guarda de código da RN23, e a subtração é a segunda camada. - Toda permissão nova tem endpoint que a exige (seção 12). Nenhuma permissão existente é reutilizada para governar este recurso.
| Endpoint | Permissão |
|---|---|
GET /agency-settlements | agency_settlements:read |
GET /agency-settlements/by-agency/{partner_agency_id} | agency_settlements:read |
GET /agency-settlements/{partner_agency_id}/{reference_month} | agency_settlements:read |
GET /agency-settlements/{partner_agency_id}/{reference_month}/billings | agency_settlements:read |
PUT /agency-settlements/{partner_agency_id}/{reference_month}/billings/{billing_id}/declaration | agency_settlements:declare |
POST /agency-settlements/{partner_agency_id}/{reference_month}/declare-remaining | agency_settlements:declare |
POST /agency-settlements/{partner_agency_id}/{reference_month}/transfers | agency_settlements:manage |
DELETE /agency-settlements/{partner_agency_id}/{reference_month}/transfers/{transfer_id} | agency_settlements:manage |
POST /agency-settlements/{partner_agency_id}/{reference_month}/close | agency_settlements:manage |
POST /agency-settlements/{partner_agency_id}/{reference_month}/reopen | agency_settlements:manage |
POST /agency-settlements/{partner_agency_id}/{reference_month}/invoice | agency_settlements:manage |
GET /documents/{id}/download (nota do acerto) | documents:read mais o escopo da RN24 |
- RN23 — Guarda de código contra o
ADMIN_GLOBAL. Todo método do service de acerto começa recusandocurrent_user.is_admin_globalcom403 PLATFORM_ADMIN_CANNOT_READ_SETTLEMENTS— mesmo predicado de_garantir_que_pode_agregar, e não "sem tenant", pelo motivo que está lá (billing_service.py:455-473).
10.3 Isolamento por garantidora (tenant)¶
- Nenhuma FK vem no corpo. Os identificadores chegam no path:
partner_agency_id,reference_monthe, na declaração,billing_id. O corpo só traz datas, valores, texto e o arquivo. partner_agency_iddo path é buscado emPartnerAgencycom otenant_iddo usuário e oagency_scopedele: imobiliária de outro tenant, inexistente ou fora do escopo →404 SETTLEMENT_NOT_FOUND— 404, nunca 403 (RN4 da 0034). O service levantaAppError(code="SETTLEMENT_NOT_FOUND", message="Acerto não encontrado.", status_code=404)diretamente — não usaresolve_in_scope, cujoNotFoundError(label)deriva o código do rótulo (apps/api/app/core/exceptions.py:44-53) e não produziria este código. O mesmo vale paraBILLING_NOT_IN_SETTLEMENTeTRANSFER_NOT_FOUND.- Imobiliária removida logicamente (
PartnerAgencytemSoftDeleteMixin) continua resolvendo no path do acerto, para usuário da garantidora: um acerto aberto com transferências não pode ficar inalcançável porque a parceira foi removida. Para o usuário do canal nada muda — a imobiliária dele é a dele. - Acerto que não existe (RN2: sem membro e sem registro) → o mesmo
404 SETTLEMENT_NOT_FOUND— idêntico ao de outro tenant, para não distinguir "não existe" de "não é seu". billing_iddo path é resolvido como membro do acerto já resolvido: a cobrança precisa ser do tenant, da imobiliária e da competência do path e satisfazer a RN1. Não sendo — de outro tenant, de outra imobiliária, de outra competência, de canal da garantidora, removida, cancelada — →404 BILLING_NOT_IN_SETTLEMENT. Cobrança de canal da garantidora do próprio acerto também é 404 aqui (não é membro), e não 403: para o chamador ela é linha de leitura, não alvo de declaração.- Todas as consultas filtram por
tenant_id = current_user.tenant_idexplicitamente — o service não aceitatenant_idnulo depois da RN23. - As tabelas novas têm
tenant_id(TenantMixin):agency_settlementseagency_settlement_transfers. A transferência é sempre lida pelo acerto, já escopado.
10.4 Isolamento por imobiliária¶
- O vínculo vem do usuário, nunca do corpo nem do path quando quem chama é do canal. Para usuário com
agency_scopenão nulo, opartner_agency_iddo path precisa ser igual aagency_scope(user); diferente →404 SETTLEMENT_NOT_FOUND, o mesmo de imobiliária inexistente (RN10/RN12 da 0034). GET /agency-settlements?reference_month=para usuário do canal devolve só a linha da própria imobiliária (a consulta já filtra poragency_scope) — nunca as outras, nem contagem delas.GET /agency-settlements/by-agency/{id}para usuário do canal com outro id → 404.- Membros: os membros e as linhas de leitura são sempre das garantias da imobiliária do path (RN1), e o path já foi conferido contra o escopo — não há cobrança de outra parceira que um usuário do canal alcance por aqui.
- RN24 — A nota fiscal herda o escopo do acerto (RN13 da 0034). O recorte de documento por imobiliária é
_owned_by_agency(apps/api/app/repositories/document_repo.py:17-54) — umORde uma subconsulta por FK de dono —, usado na listagem, no metadado, no download e na contagem de armazenamento. Ele ganha a sexta subconsulta: documento cujoagency_settlement_idaponta para acerto compartner_agency_idigual ao da imobiliária. Os doiscol_mapdo repositório — o da listagem por entidade (document_repo.py:78-83) e o do contador (:191-200, que indexa sem.get) — ganham a chave"agency_settlement". Upload de nota pela rota genérica (POST /documentscomentity_type = "agency_settlement") é recusado com422 INVALID_ENTITY_TYPE— o único caminho de escrita éPOST .../invoice, que exigeagency_settlements:manage; sem isso, odocuments:createque a imobiliária tem (rbac_seed.py:432) lhe deixaria anexar nota no próprio acerto. Nenhuma escrita sobre documento de acerto passa pela rota genérica, para ninguém —PATCH /documents/{id},PUT /documents/{id}/contenteDELETE /documents/{id}→403 AGENCY_SETTLEMENT_DOCUMENT_READ_ONLY, inclusive para oADMIN_TENANT. A guarda vale nos quatro caminhos de escrita dodocument_service(upload,update,replace_content,delete), como a do comprovante (_garantir_que_o_comprovante_pode_ser_escrito,document_service.py:315-360) já faz e pelo mesmo motivo: oOPERATORtemdocuments:updateedocuments:delete(rbac_seed.py:361) e não temagency_settlements:manage, e a imobiliária temdocuments:update. Trocar a nota éPOST .../invoice(RN17), que audita. - Usuário da garantidora (sem
agency_scope) alcança todas as imobiliárias do tenant (RN14 da 0034).
10.5 Plataforma × cliente (SaaS admin)¶
ADMIN_GLOBALnão alcança nada desta spec (RN23), nem lista, nem detalhe, nem documento de acerto — o acerto é dado de negócio do cliente, e a 0054 já negou a ele a soma da carteira (RN26a). Cross-tenant aqui seria o pior caso: o fluxo de caixa de cada garantidora com cada parceira.- Documento de acerto pelo
ADMIN_GLOBALviaGET /documents/{id}e/download→ 404, como qualquer documento de cliente para ele hoje: oADMIN_GLOBALtemtenant_idnulo, e o repositório de documento filtraDocument.tenant_id == tenant_idestrito na listagem, no metadado e no download (document_repo.py:72,:100,:136). Esta spec não muda isso e não acrescenta guarda — a recusa já existe e é a mais fechada possível. - Nenhuma tela do SaaS admin muda. Nenhuma tela da garantidora aceita
tenant_id. - O que muda no que cada perfil vê: a aba Repasses em Cobranças (para quem tem
agency_settlements:reade lê Cobranças) e a aba Repasses em Minha Imobiliária (para o canal). Nenhum item de menu novo.
11. Impacto por app¶
apps/api¶
- Modelos:
app/models/agency_settlement.py(novo) —AgencySettlement(agency_settlements):tenant_id,partner_agency_id(FKpartner_agencies.id,RESTRICT),reference_month(Text, não nulo),status(enumagency_settlement_status:OPEN,CLOSED; padrãoOPEN),closed_at,closed_by(FKusers.id,RESTRICT, anulável),invoice_number(String(60), anulável);UniqueConstraint(tenant_id, partner_agency_id, reference_month). EAgencySettlementTransfer(agency_settlement_transfers):tenant_id,settlement_id(FK,RESTRICT),received_on,amount(Numeric(12,2),CheckConstraint amount > 0),notes,created_by;app/models/billing.py—agency_outcome(enum novoagency_outcome),agency_paid_on,agency_late_fee,agency_declared_by,agency_declared_at,agency_settlement_id(RN5, RN12);app/models/document.py— sexta FKagency_settlement_id, eentity_type/entity_idpassam a reconhecê-la (RN17).- Migration
o3v5y7a9c1e3_acerto_com_imobiliaria(sucessora deq1e3m5a7i9l2, a do fix 0023 — eran2u4x6z8b0d2, a head quando a spec foi escrita): cria os dois enums e as duas tabelas, as seis colunas embillings(índice emagency_settlement_id) e a coluna emdocuments(índice).downgrade: remove a coluna dedocuments, as seis debillings, as duas tabelas e os dois enums, nessa ordem. Sem migração de dado (RN18). Destrutiva nodowngrade: perde declarações, transferências e notas — aceitável sem base instalada, e dito no docstring. - Schemas —
app/schemas/agency_settlement.py(novo):AgencySettlementRow,AgencySettlementListResponse,AgencySettlementDetail,SettlementMember,DeclarationRequest,DeclareRemainingResponse,TransferRequest,ReopenRequest.app/schemas/billing.py:BillingResponse/BillingListItemganhamagency_outcome,agency_settlement_idepartner_agency_id(leitura) — este último é o que o link Ver acerto precisa para montar a URL; hoje só vempartner_agency_name(apps/api/app/schemas/billing.py:485). - Repositório —
app/repositories/agency_settlement_repo.py(novo): membros por(tenant, imobiliária, competência)com ojoinemguarantees; acertos derivados por competência e por imobiliária (agrupamento sobre membros,UNIONcom registros gravados);get_or_createda RN3. - Domínio —
app/domain/acerto_com_imobiliaria.py(novo): RN4, RN9, RN9a, puros. - Service —
app/services/agency_settlement_service.py(novo): RN2–RN14, RN17, RN19–RN23; escopo pela seção 10.app/services/billing_service.py: extrair o passo de gravação da baixa para método comum (RN12); RN14 (recusas de membro de acerto fechado empay,update,mark_overdue,delete,restore, primeiras e sobwith_for_update()); RN15.app/repositories/document_repo.py: a sexta subconsulta de_owned_by_agencye a chave nos doiscol_map(RN24).app/services/document_service.py:agency_settlementcomo tipo conhecido mas não aceito no upload genérico, e a guarda emupdate,replace_contentedelete(RN24). A substituição da nota (RN17) remove o documento anterior pelo repositório, dentro do service do acerto — não pordocument_service.delete, que a guarda nova recusaria. - Router —
app/api/v1/routers/agency_settlements.py(novo), comCommitRoute(apps/api/app/core/transaction.py:68) — todos os endpoints de escrita gravam —,summary,descriptioneresponse_descriptionem cada um; registrado emapp/api/v1/__init__.py.GET /by-agency/{partner_agency_id}é declarada antes deGET /{partner_agency_id}/{reference_month}— na ordem inversa,by-agencycasaria comopartner_agency_ide a resposta seria422de UUID inválido. - Reutilizados sem alteração —
app/repositories/busca.py(a busca sem acento da RN20);app/domain/resumo_de_cobranca.py(o desenho que o domínio novo copia, RN9a; não muda); eapps/api/tests/test_cobranca_baixa.py, que precisa continuar verde sem edição — é a prova de que a RN15 recusa pelochannelprevisto e não pelopayment_channel. - Seed —
app/db/rbac_seed.py: as três permissões, os papéis da 10.2 e a subtração doADMIN_GLOBAL. - Testes (
apps/api/tests/, novos):test_acerto_contas.py(domínio, RN4/RN9/RN9a),test_acerto_declaracao.py(RN5–RN8b),test_acerto_fechamento.py(RN10–RN14, RN16),test_acerto_baixa_avulsa.py(RN15),test_acerto_nota_fiscal.py(RN17, RN24),test_acerto_autorizacao.py(a matriz da 10.1 inteira, linha por linha, incluindo cada negativo, o parceiro sem vínculo e oADMIN_GLOBAL),test_acerto_listas.py(RN19–RN22). O meta-teste de permissão órfã (make preflight) cobre as três permissões novas.
apps/web¶
- Páginas/componentes:
src/pages/billings/index.tsx— terceira abarepasses(?aba=repasses), visível para quem temagency_settlements:read; nela somem os filtros Tipo, Canal, Situação, os indicadores, o resumo e a ação Exportar, e a busca passa a ser por imobiliária;src/pages/billings/components/RepassesDaCompetencia.tsx(novo) — a tabela/cartões da 7.1/7.2;src/pages/agency-settlements/(novo) —show.tsx(o acerto, rota/repasses/:partnerAgencyId/:referenceMonth),components/PaineisDoAcerto.tsx,components/MembrosDoAcerto.tsx,components/DeclaracaoDialog.tsx(o diálogo de tela cheia do mobile e a edição na linha do desktop),components/TransferenciaDialog.tsx,components/NotaFiscalDialog.tsx,components/ReabrirDialog.tsx,api.ts;src/pages/my-agency/index.tsx—PageTabscom Dados (o conteúdo de hoje, intacto) e Repasses (?aba=repasses), esta só comagency_settlements:read;src/pages/my-agency/RepassesDaImobiliaria.tsx(novo);- testes unitários ao lado de cada arquivo novo (
*.test.tsx), e acréscimos empages/billings/index.test.tsxepages/my-agency/index.test.tsx(spec 0047). - Ações de cobrança que a API passa a recusar (RN14, RN15) — em
src/pages/billings/index.tsx(lista e cartões) e emsrc/pages/guarantees/components/BillingList.tsx(detalhe da garantia): Registrar pagamento não é renderizado em cobrança comchannel = AGENCY_PIX; Registrar pagamento, Marcar inadimplente, Remover e Restaurar não são renderizados em cobrança comagency_settlement_idnão nulo. Nas duas, a cobrança de canal imobiliária mostra no lugar um link curto Ver acerto para/repasses/:partnerAgencyId/:referenceMonth, para quem temagency_settlements:read. É a regra da Parte II, 7.3: ação negada não aparece. Testes unitários empages/billings/index.test.tsxepages/guarantees/components/BillingList.test.tsx. - Rotas —
App.tsx:/repasses/:partnerAgencyId/:referenceMonthdentro de<ExigeAcesso permissao="agency_settlements:read">. Quem digita a URL sem a permissão vê o acesso negado de sempre; com a permissão e fora do escopo, o alerta de não encontrado da API (404). Nenhum item novo no menu (nav-groups.tsxintocado); a tela entra na lista fora do menu do inventário de telas (apps/web/CLAUDE.md). - Ações por permissão — botões de declarar e o lote só com
agency_settlements:declaree acerto aberto; Registrar transferência, Fechar competência, Anexar nota fiscal e Reabrir só comagency_settlements:manage— não renderizados, nunca desabilitados, sem a permissão (Parte II, 7.3). O Fechar competência desabilitado (com a permissão, sem a condição) mostra o motivo a partir deundeclared,falta,overpaidelate_membersdo detalhe. data-testid(novos):billings-tab-repasses,settlements-list,settlement-row-{partnerAgencyId},settlements-search,settlements-empty,settlement-page,settlement-status-badge,settlement-close-reason,settlement-close,settlement-close-dialog-confirm,settlement-reopen,settlement-reopen-dialog-reason-input,settlement-reopen-dialog-confirm,settlement-invoice-attach,settlement-invoice-replace,settlement-invoice-number,settlement-invoice-file,settlement-invoice-confirm,settlement-invoice-view,settlement-panel-{previsao,pagamentos,acerto},settlement-devido,settlement-recebido,settlement-falta,settlement-transfer-add,settlement-transfer-date,settlement-transfer-amount,settlement-transfer-confirm,settlement-transfer-{id},settlement-transfer-remove-{id},settlement-transfer-remove-dialog-confirm,settlement-late-members,billing-settlement-link-{billingId},settlement-members,settlement-member-{billingId},settlement-member-paid-{billingId},settlement-member-unpaid-{billingId},settlement-member-adjust-{billingId},settlement-declaration-paid-on,settlement-declaration-late-fee,settlement-declaration-confirm,settlement-members-error,settlement-declare-remaining,settlement-declare-remaining-dialog-confirm,settlement-history,my-agency-tabs,my-agency-tab-repasses,my-agency-settlements,my-agency-settlement-row-{referenceMonth}.- Responsividade & PWA — como a Parte II: cartões abaixo do breakpoint (
useIsMobile), painéis Previsão e Pagamentos recolhidos no mobile (MobileDetailSectioncolapsável), sóAGENCY_PIXna lista do mobile (filtrochannelda RN21b), diálogos em tela cheia,ListPaginationno rodapé. Sem offline; o service worker não muda.
apps/e2e¶
- Spec
tests/e2e/repasses-por-imobiliaria.spec.ts(novo): a jornada da Parte II de ponta a ponta — o responsável da imobiliária declara pelo lote e corrige uma exceção (uma atrasada com multa, uma não pagou), a garantidora registra duas transferências, fecha, confere as baixas e o atraso em Cobranças, anexa a nota, reabre e vê as baixas desfeitas; o mesmo da imobiliária em 375px (cartões, botões Pagou/Não pagou); e os negativos de tela:AGENCY_USERsem botões,VIEWERsem ações, imobiliária B abrindo o acerto da A por URL e vendo o não encontrado. - Seeds — sem arquivo novo em
tests/fixtures/seeds/: o spec monta o próprio mundo pela API. Os ajudantes quebillings.spec.tstem como funções locais (imobiliariaDoTeste,:190;imobiliariaQueRecolhe,:203-207;emitirEAssinar,:238) são extraídos paratests/fixtures/cobranca.ts(novo) e importados pelos dois specs;imobiliariaQueRecolheganha o parâmetro de nome queimobiliariaDoTestejá tem, e o spec novo usa um nome próprio — a imobiliária fixa de hoje é compartilhada com outro spec. A competência usada é a do primeiro vencimento já passado das mensalidades geradas. - Usuários — o
AGENCY_MANAGERe oAGENCY_USERda imobiliária do spec são criados no próprio spec pelo fluxo de convite e aceite, chamado pela API — o mesmo fluxo queusuarios-da-imobiliaria.spec.ts:45-48percorre pela tela.
12. Contrato de API¶
Prefixo /api/v1. Valores monetários como texto decimal ("6300.00"), como no resumo da 0054. Todos exigem Authorization: Bearer. Para todos: 401 sem token; 403 sem a permissão do endpoint (require_permission); 403 PLATFORM_ADMIN_CANNOT_READ_SETTLEMENTS para ADMIN_GLOBAL (RN23); 422 INVALID_REFERENCE_MONTH (RN19); 404 SETTLEMENT_NOT_FOUND para imobiliária fora do escopo ou acerto inexistente (10.3, 10.4).
GET /agency-settlements?reference_month=2026-09&search=sol — agency_settlements:read — RN20, RN20a. reference_month obrigatório (ausente → 422 REFERENCE_MONTH_REQUIRED, o código que o resumo já usa).
{
"reference_month": "2026-09",
"truncated": false,
"items": [{
"partner_agency_id": "uuid", "partner_agency_name": "Solar Imóveis",
"situation": "OPEN", "members": 26, "declared": 24, "partial": true,
"devido": "6300.00", "recebido": "4000.00", "falta": "2300.00",
"invoice_number": null
}]
}
GET /agency-settlements/by-agency/{partner_agency_id}?skip=0&limit=12 — agency_settlements:read — RN21. Resposta paginada (items, total, page, page_size, pages, como PaginatedResponse), itens com reference_month no lugar do nome.
GET /agency-settlements/{partner_agency_id}/{reference_month} — agency_settlements:read — RN21a.
{
"partner_agency_id": "uuid", "partner_agency_name": "Solar Imóveis",
"reference_month": "2026-09", "situation": "READY",
"closed_at": null, "closed_by_name": null,
"invoice_number": null, "invoice_document_id": null,
"undeclared": 0, "partial": false, "late_members": 0, "overpaid": false,
"previsao": [{"fee_type": "GUARANTEE", "channel": "AGENCY_PIX", "amount": "6629.49"}],
"pagamentos": [{"fee_type": "GUARANTEE", "channel": "AGENCY_PIX", "amount": "6280.00"},
{"fee_type": "LATE_FEE", "channel": "AGENCY_PIX", "amount": "20.00"}],
"devido": "6300.00", "recebido": "6300.00", "falta": "0.00",
"transfers": [{"id": "uuid", "received_on": "2026-10-03", "amount": "4000.00",
"notes": null, "created_by_name": "Fulana"}],
"history": [{"action": "TRANSFER_REGISTERED", "at": "2026-10-03T14:02:00Z",
"user_name": "Fulana", "description": "R$ 4.000,00 recebidos em 03/10/2026"}]
}
"fee_type": "LATE_FEE" existe só nesta resposta, para a linha Multa; não é valor de BillingFeeType.
GET /agency-settlements/{partner_agency_id}/{reference_month}/billings?channel=AGENCY_PIX&skip=0&limit=25 — agency_settlements:read — RN21b. channel opcional (valor de BillingChannel, senão 422). Paginada.
{ "items": [{
"billing_id": "uuid", "guarantee_id": "uuid", "guarantee_number": "GAR-2026-0042",
"renter_name": "Inquilino Exemplo 02", "installment_number": 5, "installment_total": 12,
"channel": "AGENCY_PIX", "due_date": "2026-09-10", "amount": "550.00",
"status": "PENDING", "settled_outside": false, "joined_after_close": false,
"agency_outcome": "PAID", "agency_paid_on": "2026-09-18", "agency_late_fee": "12.30",
"declared_by_name": "Fulano", "declared_by_side": "AGENCY",
"late_fee_today": "15.10"
}], "total": 26, "page": 1, "page_size": 25, "pages": 2 }
PUT /agency-settlements/{pa}/{rm}/billings/{billing_id}/declaration — agency_settlements:declare — RN5–RN8.
200 com o membro (forma da RN21b). Erros: 404 BILLING_NOT_IN_SETTLEMENT; 409 SETTLEMENT_CLOSED; 422 BILLING_ALREADY_SETTLED, DECLARATION_FIELDS_REQUIRED, DECLARATION_DATE_IN_FUTURE, DECLARATION_LATE_FEE_EXCEEDS_CHARGE (details: [{"field": "late_fee", "max": "15.10"}]), DECLARATION_FIELDS_NOT_ALLOWED, UNPAID_NOT_YET_DUE, DECLARATION_BELOW_RECEIVED (RN5a); 403 AGENCY_LINK_REQUIRED.
POST /agency-settlements/{pa}/{rm}/declare-remaining — agency_settlements:declare — RN8a. Sem corpo. 200 {"marked": 24, "skipped_not_due": 0}. Erros: 409 SETTLEMENT_CLOSED; 403 AGENCY_LINK_REQUIRED.
POST /agency-settlements/{pa}/{rm}/transfers — agency_settlements:manage — RN10. {"received_on": "2026-10-03", "amount": "4000.00", "notes": null} → 201 com o detalhe (forma de RN21a). Erros: 409 SETTLEMENT_CLOSED; 422 TRANSFER_DATE_IN_FUTURE, TRANSFER_EXCEEDS_REMAINING (details: [{"field": "amount", "max": "2300.00"}]), 422 de validação para amount ≤ 0.
DELETE /agency-settlements/{pa}/{rm}/transfers/{transfer_id} — agency_settlements:manage — RN10a. 200 com o detalhe. Erros: 404 TRANSFER_NOT_FOUND; 409 SETTLEMENT_CLOSED.
POST /agency-settlements/{pa}/{rm}/close — agency_settlements:manage — RN11, RN12. Sem corpo. 200 com o detalhe e {"paid": 24, "overdue": 2} em close_result. Erros: 409 SETTLEMENT_CLOSED; 422 SETTLEMENT_NOT_READY (details da RN11), 422 NOTHING_TO_SETTLE (RN11); 422 DECLARATION_STALE (RN12).
POST /agency-settlements/{pa}/{rm}/reopen — agency_settlements:manage — RN13. {"reason": "Inquilino 07 pagou e foi declarado como não pago"} → 200 com o detalhe. Erros: 409 SETTLEMENT_NOT_CLOSED; 422 REOPEN_REASON_REQUIRED.
POST /agency-settlements/{pa}/{rm}/invoice — agency_settlements:manage — RN17. multipart/form-data: invoice_number (texto, 1–60) e file. 200 com o detalhe. Erros: 409 SETTLEMENT_NOT_CLOSED; 422 INVOICE_NUMBER_REQUIRED, INVOICE_NUMBER_TOO_LONG; os de arquivo do upload existente (tipo não aceito, tamanho acima de 10 MB, PDF incompleto), com os códigos de document_service.py.
Alterados:
POST /guarantees/{gid}/billings/{bid}/pay—422 AGENCY_BILLING_SETTLED_BY_AGREEMENT(RN15);409 BILLING_IN_CLOSED_SETTLEMENT(RN14).PATCH /guarantees/{gid}/billings/{bid},POST /guarantees/{gid}/billings/{bid}/mark-overdue,DELETE /billings/{id},POST /billings/{id}/restore—409 BILLING_IN_CLOSED_SETTLEMENT(RN14).restorede cobrança semagency_settlement_idque cai num acerto fechado é aceito e a torna membro tardio (RN14a).POST /documentscomentity_type = "agency_settlement"→422 INVALID_ENTITY_TYPE;PATCH /documents/{id},PUT /documents/{id}/contenteDELETE /documents/{id}de documento de acerto →403 AGENCY_SETTLEMENT_DOCUMENT_READ_ONLY, para qualquer papel;GET /documents/{id}e/downloadde documento de acerto: escopo da RN24;ADMIN_GLOBALrecebe 404, como hoje (10.5).BillingResponseeBillingListItemganhamagency_outcome,agency_settlement_idepartner_agency_id.
Migration o3v5y7a9c1e3_acerto_com_imobiliaria — ver seção 11; downgrade desfaz na ordem inversa e perde os dados das colunas e tabelas novas.
13. Critérios de aceite¶
Contas e regras
- [x] CA1. Dado um acerto com 3 membros em aberto, dois declarados
PAID(um com multa R$ 10,00) e um sem declaração, o detalhe mostradevido= soma dos doisamount+ 10,00,partial = trueesituation = "OPEN"(RN4, RN9). - [x] CA2. Cobrança de canal da garantidora da mesma imobiliária e competência aparece em
previsaoe na lista de membros comchannelde boleto, e não entra emdevido; cobrançaAGENCY_PIXde outra imobiliária não aparece em lugar nenhum do acerto (RN1, RN9a). - [x] CA3. Declarar
PAIDcompaid_onamanhã →422 DECLARATION_DATE_IN_FUTURE; comlate_feeacima do encargo da data →422 DECLARATION_LATE_FEE_EXCEEDS_CHARGEcom o teto emdetails;UNPAIDem cobrança que vence amanhã →422 UNPAID_NOT_YET_DUE(RN7, RN8). - [x] CA4. Declarar duas vezes a mesma cobrança, primeiro pelo
AGENCY_MANAGERe depois peloADMIN_TENANT, deixa só a segunda, comdeclared_by_side = "TENANT"(RN5). - [x] CA5. O lote marca todos os membros em aberto sem declaração como
PAIDno vencimento, sem multa; não altera um já declaradoUNPAID; pula o de vencimento futuro e o conta emskipped_not_due(RN8a). - [x] CA6. Transferência acima da
falta→422 TRANSFER_EXCEEDS_REMAINING; duas transferências que somam odevidozeram afaltae levam a situação aREADY(RN9, RN10, RN4). - [x] CA7. Fechar com membro sem declaração, com
falta > 0ou comfalta < 0→422 SETTLEMENT_NOT_READYcomundeclared/faltacorretos emdetails, e nada muda no banco; acerto só com membros baixados fora →422 NOTHING_TO_SETTLE(RN11). - [x] CA8. Fechar um acerto pronto: cada
PAIDdeclarado ficaPAIDcompaid_on= data declarada,payment_channel = AGENCY_PIX,guarantee_paid = amount,charge_paid = multa; cadaUNPAIDficaOVERDUE; todos comagency_settlement_id; o resumo da competência da 0054 passa a contar os pagos emrecebidoe não mais ema_repassar_imobiliaria(RN12). - [x] CA9. Reabrir sem motivo →
422 REOPEN_REASON_REQUIRED; com motivo, os membros voltam aPENDINGcom os campos de baixa nulos, as declarações, as transferências e a nota permanecem, e o histórico mostraREOPENEDcom o motivo (RN13, RN22). - [x] CA10. Em acerto fechado: declarar, lote, registrar e remover transferência →
409 SETTLEMENT_CLOSED;pay,update,mark_overdue,deleteerestorede um membro baixado pelo fechamento →409 BILLING_IN_CLOSED_SETTLEMENT, antes de qualquer outro código (RN14). - [x] CA10a. Emitir uma garantia com início retroativo cuja mensalidade cai numa competência já fechada daquela imobiliária: a emissão passa, o detalhe do acerto mostra
late_members = 1e o membro comjoined_after_close = true; declará-lo →409 SETTLEMENT_CLOSED; reabrir, declarar e fechar de novo o baixa. O mesmo comrestorede um membro removido antes do fechamento (RN14a). - [x] CA10b. Com
devido = recebido = 6.300,00, corrigir uma declaração dePAIDparaUNPAID→422 DECLARATION_BELOW_RECEIVEDe nada muda; remover a transferência de 550,00, corrigir, e afaltafica zero de novo (RN5a, RN10a). - [x] CA10c. Uma declaração com multa no teto, seguida de redução do percentual de multa nos Parâmetros da garantidora, e fechar →
422 DECLARATION_STALEcom obilling_ideLATE_FEE_ABOVE_MAX, sem baixa nenhuma; umaUNPAIDcujo vencimento foi editado para o futuro depois da declaração →422 DECLARATION_STALEcomNOT_YET_DUE(RN12). - [x] CA10e. Com transferências somando o
devido, remover (DELETE /billings/{id}) um membro declaradoPAID: o detalhe passa afalta < 0eoverpaid = true; fechar →422 SETTLEMENT_NOT_READY; remover a transferência a mais e registrar a correta leva aREADY(RN9, RN10a, RN11). - [x] CA10d. A trava da RN3 existe: o método que obtém o acerto para escrita usa
begin_nested()+IntegrityError+with_for_update(), e todo endpoint de escrita do acerto passa por ele — verificado por leitura na revisão de implementação, e não por teste. Limite declarado: a suíte da API roda em SQLite em memória (apps/api/tests/conftest.py:54), que ignoraFOR UPDATEe serializa tudo numa conexão, então um teste de concorrência ali passaria com ou sem a trava, e não provaria nada (RN3). - [x] CA11.
payem cobrança comchannel = AGENCY_PIX→422 AGENCY_BILLING_SETTLED_BY_AGREEMENT;payem cobrança de boleto compayment_channel = AGENCY_PIXcontinua200(RN15). - [x] CA12. A rotina de atraso continua sem marcar
AGENCY_PIXvencida e não declarada (RN16). - [x] CA13. Anexar nota em acerto aberto →
409 SETTLEMENT_NOT_CLOSED; em fechado grava número e documento; anexar de novo substitui o número e remove logicamente o documento anterior (RN17). - [x] CA14. A lista por competência traz uma imobiliária com
collects_monthly = falseque tem membros na competência, e não traz uma sem membros e sem registro (RN2, RN20a). - [x] CA15. Ler o detalhe de um acerto sem registro não cria linha em
agency_settlements; a primeira declaração cria uma (RN2, RN3).
Autorização — um negativo por papel afetado
- [x] CA16.
ADMIN_GLOBALrecebe403 PLATFORM_ADMIN_CANNOT_READ_SETTLEMENTSemGET /agency-settlements, no detalhe e nos membros — mesmo com a fixture que concede todas as permissões (RN23) —, e404noGET /documents/{id}/downloadda nota (10.5). - [x] CA17.
ANALYST,OPERATOReVIEWERleem lista, detalhe, membros e nota (200), e recebem403em declarar, lote, transferência, fechar, reabrir e anexar nota (10.1). - [x] CA18.
AGENCY_USERda Imobiliária A lê o acerto da A (200) e recebe403ao declarar e no lote;404 SETTLEMENT_NOT_FOUNDno detalhe, nos membros e na lista por imobiliária da B (10.4). - [x] CA19.
AGENCY_MANAGERda A declara no acerto da A (200); recebe404 SETTLEMENT_NOT_FOUNDao declarar no da B;404 BILLING_NOT_IN_SETTLEMENTao declarar, no path da A, obilling_idde uma cobrança da B;403em transferência, fechar, reabrir e anexar nota (10.1, 10.3, 10.4). - [x] CA20.
GET /agency-settlements?reference_month=chamado peloAGENCY_MANAGERda A devolve só a linha da A, numa competência em que A e B têm membros (10.4). - [x] CA21.
ADMIN_TENANTda garantidora X recebe404 SETTLEMENT_NOT_FOUNDno detalhe de uma imobiliária da garantidora Y, e404 BILLING_NOT_IN_SETTLEMENTao declarar uma cobrança de Y sob o path de uma imobiliária de X (10.3). - [x] CA22. Parceiro sem vínculo recebe lista vazia na leitura e
403 AGENCY_LINK_REQUIREDao declarar (10.1). - [x] CA23.
AGENCY_USERda B recebe404noGET /documents/{id}/downloadda nota do acerto da A;POST /documentscomentity_type = "agency_settlement"→422 INVALID_ENTITY_TYPEpara qualquer papel;PATCH /documents/{id}da nota peloAGENCY_MANAGERda A,DELETE /documents/{id}da nota peloOPERATORe peloADMIN_TENANT→403 AGENCY_SETTLEMENT_DOCUMENT_READ_ONLY, e a nota continua lá; oAGENCY_USERda A lista e baixa a nota do próprio acerto (200) (RN24).
Jornada, viewports e estados (e2e e unitário do web)
- [x] CA24. Desktop: Cobranças → Repasses mostra a linha da imobiliária com Devido, Recebido, Falta e o selo; a linha abre o acerto com os três painéis e a lista de membros, as de boleto esmaecidas e sem botões (7.1).
- [x] CA25. A jornada inteira do caso feliz (7, passos 1–7) passa no e2e, terminando com as baixas em Cobranças do mês e a não paga em Atrasados como Em atraso, e depois da reabertura, as duas de volta (7, CA8, CA9).
- [x] CA26. 375px,
AGENCY_MANAGER: Minha Imobiliária → Repasses mostra a competência em cartão; o acerto mostra só cartões de PIX Imobiliária, com Pagou/Não pagou de 44px; tocar Pagou declara sem abrir diálogo; a página não tem rolagem horizontal (scrollWidth − clientWidth = 0) (7.2). - [x] CA27. Fechar competência desabilitado mostra o motivo na faixa de status nos dois viewports; com tudo declarado e a falta zero, habilita (7.1, 7.2).
- [x] CA28.
AGENCY_USER: o acerto não renderiza os botões de declaração nem o lote;VIEWER: não renderiza transferência, fechar, reabrir nem anexar — testes unitários do web verificam a ausência, não odisabled(7.3). - [x] CA29. O
AGENCY_MANAGERda B que digita a URL do acerto da A vê o alerta Acerto não encontrado, igual ao de uma competência sem acerto (7.3). - [x] CA30. Estado vazio da aba Repasses numa competência sem membros mostra o texto da 7.3; o erro de carga mostra Tentar de novo e esconde os painéis (7.3).
14. Plano de implementação¶
Ordem obrigatória: api → web → e2e.
- [x]
apps/api— modelos, enums e migrationo3v5y7a9c1e3comdowngradeexercitado (upgrade→downgrade→upgradeno banco do dev-remote) - [x]
apps/api— seed RBAC (10.2) - [x]
apps/api—domain/acerto_com_imobiliaria.py+ testes de domínio (TDD) - [x]
apps/api— repositório e service do acerto (RN1–RN14, RN17, RN19–RN23), TDD, comtest_acerto_autorizacao.pyantes do router - [x]
apps/api—billing_service: extração da gravação da baixa, RN14, RN15;document_repoedocument_service: RN24 - [x]
apps/api— router comCommitRoutee Swagger completo - [x]
apps/api—make preflighte a revisão da fase - [x]
apps/web— aba Repasses em Cobranças, tela do acerto, abas de Minha Imobiliária, diálogos;data-testidda seção 11; desktop e mobile juntos; testes unitários - [x]
apps/web— inventário de telas noapps/web/CLAUDE.md(tela fora do menu) - [x]
apps/web—make preflight, a revisão da fase, screenshot nos dois viewports contra o dev-remote - [x]
apps/e2e—repasses-por-imobiliaria.spec.ts - [x]
make testemake lint(sem apontamento novo nos arquivos tocados) - [x]
/devremote-rebuild, o spec e2e novo e os existentes das telas tocadas (billings.spec.ts,usuarios-da-imobiliaria.spec.ts) - [x]
ROADMAP.md— E2 marcado como entregue
15. Decisões em aberto¶
- [x] Quem emite a nota fiscal contra quem — o usuário não sabe ainda (entrevista de 2026-10-01). Aceito conscientemente: a spec só guarda número e arquivo (RN17), e nada no desenho depende da direção. Decide-se quando houver integração com emissor.
16. Revisão do detalhamento¶
Preenchido pelo laço do /spec new, rubrica .claude/rubrics/detalhamento.md. Registrar todas as rodadas, inclusive as reprovadas.
| Rodada | Critical | Important | Minor | Nota | Veredicto |
|---|---|---|---|---|---|
| 1 | 0 | 5 | 7 | 7 | Reprovada — falta podia ficar negativa e travar o acerto; caminhos reais de entrada em acerto fechado descobertos; mecanismo de escopo do documento errado; endpoint de encargo negado sem razão; botões de baixa na tela contra a RN15. Duas decisões levadas ao usuário |
| 2 | 1 | 4 | 7 | 7 | Reprovada — o OPERATOR apagaria a nota pelo DELETE genérico; falta negativa alcançável pelo lado da cobrança; UNPAID sem revalidação; prova de concorrência impossível em SQLite; campos de contrato faltando |
| 3 | 0 | 2 | 5 | 8 | Aprovada — os dois Important (RN10 contradizendo a RN9; RN11 avaliada antes da releitura travada) e os cinco Minor corrigidos no texto antes de fechar |
Resultado: aprovada na rodada 3, nota 8.
Riscos aceitos: nenhum.
Pendências aceitas: nenhuma — os achados da rodada 3 foram corrigidos no texto. Fica registrado o limite declarado do CA10d: a trava da RN3 se verifica por leitura, porque a suíte da API roda em SQLite.
17. Revisão da implementação¶
Preenchido pelo /spec implement, rubrica .claude/rubrics/implementacao.md. As revisões de fase (api, web, e2e) vão nas primeiras linhas; a da branch inteira, na última.
| Rodada | Escopo | Critical | Important | Minor | Nota | Veredicto |
|---|---|---|---|---|---|---|
| 1 | apps/api | 0 | 2 | 10 | 8 | Aprovada — os dois Important (nota com número vazio caía no 422 genérico do Form; faltavam negativos de isolamento na transferência e nas escritas) corrigidos e cobertos antes da fase web, com red-green |
| 2 | apps/web | 0 | 4 | 12 | 7 | Reprovada — erro da lista de mensalidades engolido; data-testid duplicado entre botão e diálogo; negativo do CA28 sem o acerto fechado; formato da nota não conferido antes do envio. Os quatro corrigidos e cobertos, e oito dos Minor também (§18); a revisão da branch reavalia |
| 3 | branch | 0 | 3 | 13 | 7 | Reprovada — a purga de tenant não conhecia as tabelas novas (FK RESTRICT); a multa sugerida não acompanhava a data, e o e2e que dizia cobrir "atrasada com multa" declarava zero; faltava o negativo 403 do DELETE de transferência. Os três corrigidos com red-green |
| 4 | branch | 0 | 2 | 7 | 7 | Reprovada — o Swagger dos endpoints de cobrança e de documento que mudaram de contrato não dizia os erros novos, e o de mark-overdue dizia o contrário da RN15; o segundo Important (o afterAll do e2e não devolveria null) não procede: o PATCH /settings aceita null explícito nos dois campos anuláveis, e o banco confirmou a volta. Swagger corrigido, e quatro Minor também |
| 5 | branch | 0 | 0 | 11 | 8 | Aprovada. Corrigidos quatro Minor (filtro de Auditoria, nomes das permissões em Perfis de Acesso, descrição do filtro de documentos, motivo de não fechar visível também para a imobiliária); os demais viram defeitos aceitos abaixo, e um vai ao usuário |
Resultado: aprovada na rodada 5 da tabela (a terceira da revisão de branch), nota 8.
Defeitos aceitos (Minor da última rodada, triados por quem implementou; nenhum muda correção, isolamento ou dinheiro):
- O e2e do CA25 confere as baixas e o atraso pela API, sem abrir Cobranças do mês e Atrasados — a situação derivada que essas abas mostram já tem teste de unidade e não mudou nesta spec. O CA26 não afirma a ausência dos cartões de boleto no celular (o filtro
channel=AGENCY_PIXé coberto no unitário da API), e o CA29 usa umAGENCY_USERda outra parceira, não oAGENCY_MANAGER— no caminho de leitura dá no mesmo. - "N de M declaradas" conta em
Mas mensalidades baixadas fora do acerto (RN6), que nunca viram declaradas; só existem em dado anterior a esta spec (RN18). - Marcar restantes como pagas aparece mesmo quando só restam mensalidades de vencimento futuro, que o lote pula (RN8a) — o diálogo e a resposta dizem quantas foram marcadas.
- A recusa da RN5a na tela não traz o valor em reais que a 7.3 cita; traz o que fazer.
- A aba Repasses de Minha Imobiliária pede 25 por página, e não 12: 12 não é opção do
ListPagination, que oferece 25, 50 e 100. - Sem teste: o aviso de lista truncada (o teto de 500 da RN20) e a listagem da nota pela imobiliária (o download está coberto).
Levado ao usuário: depois do fechamento, a mensalidade declarada não paga continua no indicador A repassar pela imobiliária do resumo da competência (0054), que classifica por canal — a seção 4 diz que ela "passa a ser atraso do inquilino", e a seção 11 não mexe no resumo. Ver o Registro. Decisão do usuário (2026-10-05): fica para o E3 — quando o sinistro existir, a não paga deixa de contar como dinheiro com a imobiliária em todas as leituras.
18. Registro¶
Fase apps/api (2026-10-05).
BillingResponsenão ganhoupartner_agency_id; sóBillingListItem. A resposta por cobrança é montada das colunas da própria cobrança, que não tem a imobiliária, e a tela que a usa (o detalhe da garantia) já tem a garantia — de onde tira opartner_agency_idpara o link Ver acerto. A lista cross-garantia ganhou a coluna nojoinque ela já fazia (_base_cross_guarantee), e a exportação ignora o quinto campo.- Os testes ficaram em cinco arquivos, não sete:
test_acerto_contas.py(domínio),test_acerto_declaracao.py,test_acerto_fechamento.py,test_acerto_baixa_e_nota.py(RN15, RN17, RN24) etest_acerto_autorizacao.py(a matriz e as listas, RN19–RN22), mais o mundo compartilhado emtests/_acerto.py. O conteúdo da seção 11 está todo lá. - O mundo dos testes copia garantias no banco em vez de emitir três pelo fluxo real — o SQLite da suíte não valida FK, e emitir três exigiria três garantidoras. A entrada tardia da RN14a (CA10a) é, por isso, uma mensalidade inserida direto, não uma emissão retroativa; o que se prova é o comportamento do acerto diante dela, que não depende de como ela nasceu.
DECLARATION_FIELDS_REQUIREDentrou na RN7 e no contrato: a regra pediapaid_onelate_feeemPAIDe não nomeava o erro.- O resumo da 0054 continua contando em "a repassar" a mensalidade declarada não paga depois do fechamento (CA8): ele classifica pelo canal, e esta spec não o muda. Fica como pergunta para quando o sinistro (E3) existir — aí a não paga deixa de ser dinheiro a caminho da imobiliária e passa a ser inadimplência do inquilino em qualquer leitura.
- Pendências da revisão da fase, aceitas: a lista por imobiliária (RN21) carrega as mensalidades de todos os meses e pagina competências em memória, e a resposta da declaração relê a lista do mês para montar uma linha — os dois são custo linear no volume de uma parceira, sem efeito de correção; Pagamentos com o acerto aberto omite as mensalidades baixadas fora do acerto (RN18), que só existem em dado anterior a esta spec.
Fase apps/web (2026-10-05).
- No desktop, Pagou não abre campos na própria linha. A Parte II (7.1) descreve Pago em e Multa editáveis na linha, salvando ao sair do campo. Implementado: Pagou declara num clique, no vencimento e sem multa — o mesmo toque do celular (7.2) —, e o ícone de lápis da linha abre o diálogo de data e multa que o celular já usa. Um caminho só nos dois viewports, e nenhuma edição em célula de tabela, padrão que o app não tem.
- A contagem do diálogo de fechar ("N recebem baixa · M passam a atraso") é feita na tela, pedindo as mensalidades de PIX Imobiliária com
limit=200; o detalhe não traz as duas contagens separadas, e uma imobiliária com mais de duzentas mensalidades num mês veria o número da primeira página. Limite declarado. - O botão Exportar some só na aba Repasses; nas outras abas continua igual.
- Os
data-testiddos diálogos de confirmação levam o sufixo-dialog(settlement-close-dialog-confirm,settlement-reopen-dialog-*,settlement-declare-remaining-dialog-confirm,settlement-transfer-remove-dialog-confirm): oConfirmDialogusa o prefixo também no próprio papel do diálogo, e com o prefixo igual ao do botão que o abre os dois ficariam no DOM com o mesmo id. A seção 11 foi atualizada com os nomes reais, e com os do diálogo de declaração, que substitui os campos na linha (ver o primeiro item desta fase). - No acerto fechado com nota, o cabeçalho tem Ver nota fiscal e Reabrir; Substituir nota foi para a faixa de status, para o cabeçalho respeitar as duas ações da 7.2. Fechar competência desabilitado não tem tooltip — o
PageHeadernão o aceita —; o motivo fica na faixa de status, nos dois viewports, como a 7.1 também pede. - Pagou numa mensalidade ainda não vencida declara a data de hoje, porque a RN7 recusa data futura e o vencimento ainda não chegou; Não pagou não aparece nela, porque a RN8 a recusaria. Tocar Pagou numa já declarada paga não regrava — para mudar data ou multa, o lápis abre o diálogo com o que está gravado.
- Revisão da fase: reprovada na primeira rodada (nota 7) por quatro pontos — erro da lista de mensalidades engolido,
data-testidduplicado, negativo do CA28 sem o acerto fechado, formato da nota não conferido antes do envio. Os quatro corrigidos e cobertos, o primeiro com red-green.
Revisão da branch (2026-10-05).
- A purga de garantidora (
tenant_purge_service.delete_tenant_cascade) passou a levar o acerto — transferências e registro, depois dedocumentse antes deusers. Fora da cascata, a purga de uma garantidora que usou o acerto cairia inteira no Postgres pelas FKsRESTRICT. A spec não falava de purga; oapps/api/CLAUDE.mdexige que entidade nova com FKRESTRICTentre nela. - A multa sugerida acompanha a data enquanto a pessoa não a digita: zero até o vencimento, o encargo de hoje quando a data é hoje (RN8b); para outra data passada a tela mantém o valor, e a recusa da API diz o teto (RN7).
- O e2e liga os parâmetros de encargo do tenant no
beforeAlle os devolve noafterAll— sem eles não há multa a declarar, e o fechamento revalida contra o teto de agora (RN12), então precisam valer da declaração até o fechar. - Ver nota fiscal aparece também para a imobiliária, no cabeçalho. A 7.1 diz que a imobiliária não tem ação no cabeçalho; mas a RN24 lhe dá a leitura da nota, e sem esta ação ela não teria por onde abri-la. Só leitura.
- Os ajudantes do e2e foram para
apps/e2e/fixtures/cobranca.ts, e nãotests/fixtures/cobranca.tscomo a seção 11 dizia: é onde moramperiodo.tseroles.ts, que os specs já importam. - A migration mudou de base depois da implementação. O fix 0023 subiu antes (v0.11.2) com a
q1e3m5a7i9l2, que também saía den2u4x6z8b0d2. Mesclada amainnesta branch (2026-10-10), adown_revisiondao3v5y7a9c1e3passou aq1e3m5a7i9l2— senão oupgrade headacusaria duas heads. Nenhuma mudança de conteúdo na migration.
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 |
|---|---|---|
resumo_de_cobranca.py:288-289 soma não pagas de AGENCY_PIX em a_repassar_imobiliaria | §1 | Parte I, rodada 4 |
apps/api/app/schemas/billing.py:240 — AWAITING_AGENCY é situação derivada | §1 | Parte I, rodada 4 |
apps/api/app/schemas/billing.py:88-96 — payment_channel da baixa aceita qualquer canal, inclusive AGENCY_PIX | §1 | Parte I, rodada 4 |
billing_overdue_service.py:9-14 — a rotina de atraso exclui AGENCY_PIX | §1 | Parte I, rodada 4 |
billing_service.py:475-483 — o resumo não filtra por parceira | §1 | Parte I, rodada 4 |
rbac_seed.py:280 — billings:update só no ADMIN_TENANT; ANALYST/OPERATOR/VIEWER têm list_all sem update | §2 | Parte I, rodada 4 |
rbac_seed.py:423-428 — billings:list_all fora do papel da imobiliária | §2 | Parte I, rodada 4 |
partner_agency.py:48 — collects_monthly | §2 | Parte I, rodada 4 |
guarantee_service.py:774-775 — sem 1º mês na entrada, a mensalidade 1 já sai no canal da parceira | §2 | Parte I, rodada 4 |
ROADMAP.md:202 e :204-206 — repasse "só a Taxa TXC"; fechamento termina com NF da garantidora | §3, §4 | Parte I, rodada 4 |
Planilha: abas, C11 (1, 13, 13, 19, 26, 36), bloco R7:T13, T8/T9/H8 por mês, multas N22 e N38 | §2, §3 | Parte I, rodada 4 |
billing_service.py:57-70 (validar_competencia), :235-239 (BILLING_ALREADY_SETTLED), :270-279 (campos da baixa), :455-473 (_garantir_que_pode_agregar) | RN6, RN12, RN19, RN23 | Parte III, rodada 1 |
scoping.py:71-81, :102-121 | 10.1 | Parte III, rodada 1 |
rbac_seed.py:230-248 (subtração do ADMIN_GLOBAL), :432 (documents:create/update no canal, sem delete), :444-446 (união do AGENCY_MANAGER) | 10.2, RN24 | Parte III, rodada 1 |
apps/api/app/models/document.py:16-35, apps/api/app/core/config.py:129-130, document_service.py:115-127 | RN17 | Parte III, rodada 1 |
distribuicao_de_baixa.py:86 e :131-133, encargo.py:169, guarantee_service.py:786, apps/api/app/core/transaction.py:68 | RN7, RN12, seção 11 | Parte III, rodada 1 |
test_cobranca_baixa.py não paga cobrança com channel = AGENCY_PIX | RN15 | Parte III, rodada 1 |
guarantee_service.py:774-793 cria mensalidades pelo repositório; billing_service.py:155-163 (avulsa nasce no canal da garantidora); apps/api/app/schemas/billing.py:40-42 (BillingUpdate) | RN14a | Parte III, rodada 2 |
routers/billing.py:485-528 — /charge exige billings:update | RN8b | Parte III, rodada 2 |
document_repo.py:17-54 (_owned_by_agency), :78-83 e :191-200 (col_map) | RN24 | Parte III, rodada 2 |
apps/api/app/core/exceptions.py:44-53 — NotFoundError deriva o código do rótulo | 10.3 | Parte III, rodada 2 |
billings.spec.ts:203-207 e :238; usuarios-da-imobiliaria.spec.ts:45-48 | seção 11 | Parte III, rodada 2 |
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.