Pular para conteúdo

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_channel aceita AGENCY_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-206 termina numa NF, emitida fora do sistema; o produto não tem onde guardá-la — o Document pertence 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 com billings:update, apps/api/app/db/rbac_seed.py:280; ANALYST, OPERATOR e VIEWER leem Cobranças, com billings:list_all, mas não registram pagamento). Sente todo mês, para toda parceira que recolhe: é caminho obrigatório, não exceção — com PartnerAgency.collects_monthly ligado (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_all está 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.

  1. 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.
  2. 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, M não registra pagamento: é fórmula que copia G (M20 = G20, e M21:M33 em maio, M21:M38 em junho, como fórmula compartilhada =G21), e O fica vazia — em maio só O34, uma linha de boleto, tem data. Em julho M virou o literal 0. 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.
  3. Lança a multa, quando houve [planilha]. A multa de quem pagou atrasado vai na coluna N da 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 em K38/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.
  4. 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:
  5. a fiança — em maio T8 = SUM(G…), a soma do previsto; em junho T8 = 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 de G (passo 2);
  6. o setup — em maio T9 = H8, e H8 é 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 julho T9 é o literal 0, mesmo com H8 = 300 em 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.

  1. 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.
  2. 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.
  3. A nota fiscal — o ROADMAP.md:204-206 descreve 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:202 diz 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 ali M é 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:

  1. 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.
  2. 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.
  3. A Solar transfere o dinheiro, fora do sistema, de uma vez ou em partes.
  4. 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.
  5. 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.
  6. A nota fiscal, quando existir, é anexada ao acerto fechado — Anexar nota fiscal: número e arquivo. Até lá, a linha mostra Sem nota.
  7. 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_month igual à competência; garantia com partner_agency_id igual à imobiliária; não removidas (deleted_at nulo); e status ≠ 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_settlements nasce 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 (OPEN ou CLOSED), 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 em apps/api/app/repositories/numbering_sequence_repo.py:30-60 — begin_nested() para tentar o INSERT, IntegrityError capturado dentro do savepoint (que não aborta a transação do CommitRoute), e SELECT ... with_for_update() da linha. É construção portátil, não ON CONFLICT de 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 (falta negativa) 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 diz CLOSED. Com late_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) e falta = 0 (RN9), com devido > 0 ou ao menos um membro declarado;
  • OPEN (Aberto) — os demais; com falta < 0 (RN9), a resposta traz overpaid = true.

A declaração.

  • RN5 — Declarar é por cobrança, e fica na cobrança. Cinco colunas novas em billings, todas anuláveis: agency_outcome (enum PAID/UNPAID), agency_paid_on (data), agency_late_fee (Numeric(12,2)), agency_declared_by (FK users.id, RESTRICT) e agency_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 devido e o deixa abaixo do recebido (RN9) — declaração que mantém ou aumenta o devido passa sempre, mesmo com falta já negativa; o lote (RN8a) só aumenta, e por isso não passa por esta regra — → 422 DECLARATION_BELOW_RECEIVED, com details = [{"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á PAID antes 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 membro PAID → 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_on e late_fee obrigató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_on não futuro — futuro → 422 DECLARATION_DATE_IN_FUTURE, pela mesma razão da RN17 da 0054;
  • late_fee obrigatório, ≥ 0, e não maior que o encargo cobrável da cobrança naquela data, calculado por encargo_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 no details. Pago até o vencimento, o teto é zero;
  • o valor pago é o amount da 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_on e late_fee devem 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ão 422 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, com paid_on = due_date e late_fee = 0, todo membro em aberto sem declaração; não toca membro já declarado. Membro cujo due_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) exige billings:update, que o AGENCY_MANAGER nã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_TENANT inclusive: 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 declarados PAID e em aberto (RN6), enquanto o acerto está aberto; fechado, a conta é outra e substitui esta: Σ(amount_paid) dos membros com agency_settlement_id = o acerto (RN12). Membro tardio (RN14a), mesmo com declaração antiga, não entra no devido do fechado;
  • recebido = Σ amount das 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 declarado PAID (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 o devido cai depois de o dinheiro já ter sido registrado. Com falta < 0 o 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: Σ amount por (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_paid por balde) das PAID; as de AGENCY_PIX, Σ amount das declaradas PAID (ou baixadas pelo fechamento), e a linha Multa — PIX Imobiliária = Σ agency_late_fee das 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 a falta no details. 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 a falta abaixo 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á CheckConstraint nem invariante de domínio de falta ≥ 0.
  • RN10a — Remover transferência. Só em acerto aberto, por quem tem agency_settlements:manage. Remoção física da linha, com auditoria agency_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, com details = [{"field": "undeclared", "value": N}, {"field": "falta", "value": "X.YY"}] — lista, que é o tipo de AppError.details (apps/api/app/core/exceptions.py:37). Acerto sem membro declarado PAID e sem UNPAID (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 PAID recebe baixa pelo mesmo caminho da baixa avulsa — a distribuição de distribuir (apps/api/app/domain/distribuicao_de_baixa.py:86) com amount_paid = amount + agency_late_fee, guarantee_paid = amount (o membro é sempre fee_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 que pay grava (billing_service.py:270-279). O passo de gravação é extraído de pay para um método privado comum, para os dois caminhos não divergirem;
  • cada membro declarado UNPAID passa a OVERDUE;
  • todo membro afetado ganha agency_settlement_id = o acerto (coluna nova, FK agency_settlements.id, RESTRICT), que é o que a reabertura usa (RN13);
  • o registro vai a CLOSED, com closed_at e closed_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 falta não for zero, 422 SETTLEMENT_NOT_READY e 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 encontrar agency_settlement_id gravado (RN14) — e cada declaração é revalidada: as PAID pela RN7 com o encargo de agora (os Parâmetros podem ter mudado), e as UNPAID pela RN8 com o due_date de 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, com details = [{"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. Assim distribuir nunca levanta PAYMENT_EXCEEDS_DUE/PAYMENT_ALLOCATION_MISMATCH no fechamento;
  • auditoria: uma linha agency_settlement / CLOSED com a contagem de baixados e de atrasados, mais uma linha billing / PAID (ou OVERDUE) por membro, com o mesmo payload que a baixa avulsa grava e "via": "agency_settlement".
  • RN13 — Reabrir. Só acerto CLOSED; reason obrigatório (não vazio depois de strip) → senão 422 REOPEN_REASON_REQUIRED. Numa transação: todo membro com agency_settlement_id = este acerto volta a PENDING, com amount_paid, paid_at, paid_on, payment_channel, setup_paid, guarantee_paid, misc_paid, charge_paid, discount_amount e agency_settlement_id anulados; as declarações ficam (é para corrigi-las); transferências e nota fiscal ficam; o registro volta a OPEN com closed_at/closed_by anulados. Auditoria agency_settlement / REOPENED com o motivo e os ids devolvidos. Um membro que estava OVERDUE antes do fechamento (marcado à mão) volta a PENDING, não a OVERDUE: 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, delete e restore sobre cobrança com agency_settlement_id não nulo → 409 BILLING_IN_CLOSED_SETTLEMENT. (agency_settlement_id só é 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 de BILLING_ALREADY_SETTLED, BILLING_NOT_PENDING e da RN15, e é feita sobre a cobrança relida com with_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 fica PENDING, sem agency_settlement_id, e é um membro tardio: o detalhe a lista com joined_after_close = true e devolve late_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ó muda due_date e notes, 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}/pay sobre cobrança com channel = 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 o channel previsto, não o payment_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_overdue manual continua aceito em membro de acerto aberto.
  • RN17 — A nota fiscal. Número (invoice_number, texto, 1 a 60 caracteres depois de strip, 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 como Document com a sexta FK mutuamente exclusiva, agency_settlement_id (documents.agency_settlement_id, FK agency_settlements.id, SET NULL, como as outras cinco, apps/api/app/models/document.py:16-35). Só em acerto CLOSED → senão 409 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 de document_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_month no formato AAAA-MM, validado por validar_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_name ou name), situação (RN4), members (contagem), declared (contagem de declarados entre os em aberto), devido, parcial, recebido, falta, invoice_number. Ordenada por nome. Filtro search por nome ou nome fantasia da imobiliária, sem acento e sem caixa, com a mesma função de busca que a 0021 pôs em repositories/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 diz truncated: true se houver mais — limite declarado, não silencioso.
  • RN20a — Imobiliária sem collects_monthly hoje 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_by com nome, invoice_number e 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 opcional channel (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) e settled_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" e entity_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 com billings: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_all em ANALYST, OPERATOR, VIEWER), onde a aba vive;
  • declarar do lado da imobiliária é só do AGENCY_MANAGER, e da garantidora só do ADMIN_TENANT — decisão do usuário (entrevista de 2026-10-01; seção 7);
  • o ADMIN_GLOBAL fora 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_MANAGER ganha agency_settlements:declare no conjunto que soma ao AGENCY_USER (rbac_seed.py:444-446), não no do AGENCY_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 recusando current_user.is_admin_global com 403 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_month e, na declaração, billing_id. O corpo só traz datas, valores, texto e o arquivo.
  • partner_agency_id do path é buscado em PartnerAgency com o tenant_id do usuário e o agency_scope dele: imobiliária de outro tenant, inexistente ou fora do escopo → 404 SETTLEMENT_NOT_FOUND — 404, nunca 403 (RN4 da 0034). O service levanta AppError(code="SETTLEMENT_NOT_FOUND", message="Acerto não encontrado.", status_code=404) diretamente — não usa resolve_in_scope, cujo NotFoundError(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 para BILLING_NOT_IN_SETTLEMENT e TRANSFER_NOT_FOUND.
  • Imobiliária removida logicamente (PartnerAgency tem SoftDeleteMixin) 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_id do 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_id explicitamente — o service não aceita tenant_id nulo depois da RN23.
  • As tabelas novas têm tenant_id (TenantMixin): agency_settlements e agency_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_scope não nulo, o partner_agency_id do path precisa ser igual a agency_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 por agency_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) — um OR de uma subconsulta por FK de dono —, usado na listagem, no metadado, no download e na contagem de armazenamento. Ele ganha a sexta subconsulta: documento cujo agency_settlement_id aponta para acerto com partner_agency_id igual ao da imobiliária. Os dois col_map do 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 /documents com entity_type = "agency_settlement") é recusado com 422 INVALID_ENTITY_TYPE — o único caminho de escrita é POST .../invoice, que exige agency_settlements:manage; sem isso, o documents:create que 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}/content e DELETE /documents/{id} → 403 AGENCY_SETTLEMENT_DOCUMENT_READ_ONLY, inclusive para o ADMIN_TENANT. A guarda vale nos quatro caminhos de escrita do document_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: o OPERATOR tem documents:update e documents:delete (rbac_seed.py:361) e não tem agency_settlements:manage, e a imobiliária tem documents: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_GLOBAL nã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_GLOBAL via GET /documents/{id} e /download → 404, como qualquer documento de cliente para ele hoje: o ADMIN_GLOBAL tem tenant_id nulo, e o repositório de documento filtra Document.tenant_id == tenant_id estrito 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:read e 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 (FK partner_agencies.id, RESTRICT), reference_month (Text, não nulo), status (enum agency_settlement_status: OPEN, CLOSED; padrão OPEN), closed_at, closed_by (FK users.id, RESTRICT, anulável), invoice_number (String(60), anulável); UniqueConstraint(tenant_id, partner_agency_id, reference_month). E AgencySettlementTransfer (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 novo agency_outcome), agency_paid_on, agency_late_fee, agency_declared_by, agency_declared_at, agency_settlement_id (RN5, RN12);
  • app/models/document.py — sexta FK agency_settlement_id, e entity_type/entity_id passam a reconhecê-la (RN17).
  • Migration o3v5y7a9c1e3_acerto_com_imobiliaria (sucessora de q1e3m5a7i9l2, a do fix 0023 — era n2u4x6z8b0d2, a head quando a spec foi escrita): cria os dois enums e as duas tabelas, as seis colunas em billings (índice em agency_settlement_id) e a coluna em documents (índice). downgrade: remove a coluna de documents, as seis de billings, as duas tabelas e os dois enums, nessa ordem. Sem migração de dado (RN18). Destrutiva no downgrade: 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/BillingListItem ganham agency_outcome, agency_settlement_id e partner_agency_id (leitura) — este último é o que o link Ver acerto precisa para montar a URL; hoje só vem partner_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 o join em guarantees; acertos derivados por competência e por imobiliária (agrupamento sobre membros, UNION com registros gravados); get_or_create da 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 em pay, update, mark_overdue, delete, restore, primeiras e sob with_for_update()); RN15. app/repositories/document_repo.py: a sexta subconsulta de _owned_by_agency e a chave nos dois col_map (RN24). app/services/document_service.py: agency_settlement como tipo conhecido mas não aceito no upload genérico, e a guarda em update, replace_content e delete (RN24). A substituição da nota (RN17) remove o documento anterior pelo repositório, dentro do service do acerto — não por document_service.delete, que a guarda nova recusaria.
  • Router — app/api/v1/routers/agency_settlements.py (novo), com CommitRoute (apps/api/app/core/transaction.py:68) — todos os endpoints de escrita gravam —, summary, description e response_description em cada um; registrado em app/api/v1/__init__.py. GET /by-agency/{partner_agency_id} é declarada antes de GET /{partner_agency_id}/{reference_month} — na ordem inversa, by-agency casaria como partner_agency_id e a resposta seria 422 de 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); e apps/api/tests/test_cobranca_baixa.py, que precisa continuar verde sem edição — é a prova de que a RN15 recusa pelo channel previsto e não pelo payment_channel.
  • Seed — app/db/rbac_seed.py: as três permissões, os papéis da 10.2 e a subtração do ADMIN_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 o ADMIN_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 aba repasses (?aba=repasses), visível para quem tem agency_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 — PageTabs com Dados (o conteúdo de hoje, intacto) e Repasses (?aba=repasses), esta só com agency_settlements:read; src/pages/my-agency/RepassesDaImobiliaria.tsx (novo);
  • testes unitários ao lado de cada arquivo novo (*.test.tsx), e acréscimos em pages/billings/index.test.tsx e pages/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 em src/pages/guarantees/components/BillingList.tsx (detalhe da garantia): Registrar pagamento não é renderizado em cobrança com channel = AGENCY_PIX; Registrar pagamento, Marcar inadimplente, Remover e Restaurar não são renderizados em cobrança com agency_settlement_id nã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 tem agency_settlements:read. É a regra da Parte II, 7.3: ação negada não aparece. Testes unitários em pages/billings/index.test.tsx e pages/guarantees/components/BillingList.test.tsx.
  • Rotas — App.tsx: /repasses/:partnerAgencyId/:referenceMonth dentro 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.tsx intocado); 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:declare e acerto aberto; Registrar transferência, Fechar competência, Anexar nota fiscal e Reabrir só com agency_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 de undeclared, falta, overpaid e late_members do 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 (MobileDetailSection colapsável), só AGENCY_PIX na lista do mobile (filtro channel da RN21b), diálogos em tela cheia, ListPagination no 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_USER sem botões, VIEWER sem 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 que billings.spec.ts tem como funções locais (imobiliariaDoTeste, :190; imobiliariaQueRecolhe, :203-207; emitirEAssinar, :238) são extraídos para tests/fixtures/cobranca.ts (novo) e importados pelos dois specs; imobiliariaQueRecolhe ganha o parâmetro de nome que imobiliariaDoTeste já 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_MANAGER e o AGENCY_USER da imobiliária do spec são criados no próprio spec pelo fluxo de convite e aceite, chamado pela API — o mesmo fluxo que usuarios-da-imobiliaria.spec.ts:45-48 percorre 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.

{ "outcome": "PAID", "paid_on": "2026-09-18", "late_fee": "12.30" }
{ "outcome": "UNPAID" }

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). restore de cobrança sem agency_settlement_id que cai num acerto fechado é aceito e a torna membro tardio (RN14a).
  • POST /documents com entity_type = "agency_settlement" → 422 INVALID_ENTITY_TYPE; PATCH /documents/{id}, PUT /documents/{id}/content e DELETE /documents/{id} de documento de acerto → 403 AGENCY_SETTLEMENT_DOCUMENT_READ_ONLY, para qualquer papel; GET /documents/{id} e /download de documento de acerto: escopo da RN24; ADMIN_GLOBAL recebe 404, como hoje (10.5).
  • BillingResponse e BillingListItem ganham agency_outcome, agency_settlement_id e partner_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 mostra devido = soma dos dois amount + 10,00, partial = true e situation = "OPEN" (RN4, RN9).
  • [x] CA2. Cobrança de canal da garantidora da mesma imobiliária e competência aparece em previsao e na lista de membros com channel de boleto, e não entra em devido; cobrança AGENCY_PIX de outra imobiliária não aparece em lugar nenhum do acerto (RN1, RN9a).
  • [x] CA3. Declarar PAID com paid_on amanhã → 422 DECLARATION_DATE_IN_FUTURE; com late_fee acima do encargo da data → 422 DECLARATION_LATE_FEE_EXCEEDS_CHARGE com o teto em details; UNPAID em cobrança que vence amanhã → 422 UNPAID_NOT_YET_DUE (RN7, RN8).
  • [x] CA4. Declarar duas vezes a mesma cobrança, primeiro pelo AGENCY_MANAGER e depois pelo ADMIN_TENANT, deixa só a segunda, com declared_by_side = "TENANT" (RN5).
  • [x] CA5. O lote marca todos os membros em aberto sem declaração como PAID no vencimento, sem multa; não altera um já declarado UNPAID; pula o de vencimento futuro e o conta em skipped_not_due (RN8a).
  • [x] CA6. Transferência acima da falta → 422 TRANSFER_EXCEEDS_REMAINING; duas transferências que somam o devido zeram a falta e levam a situação a READY (RN9, RN10, RN4).
  • [x] CA7. Fechar com membro sem declaração, com falta > 0 ou com falta < 0 → 422 SETTLEMENT_NOT_READY com undeclared/falta corretos em details, e nada muda no banco; acerto só com membros baixados fora → 422 NOTHING_TO_SETTLE (RN11).
  • [x] CA8. Fechar um acerto pronto: cada PAID declarado fica PAID com paid_on = data declarada, payment_channel = AGENCY_PIX, guarantee_paid = amount, charge_paid = multa; cada UNPAID fica OVERDUE; todos com agency_settlement_id; o resumo da competência da 0054 passa a contar os pagos em recebido e não mais em a_repassar_imobiliaria (RN12).
  • [x] CA9. Reabrir sem motivo → 422 REOPEN_REASON_REQUIRED; com motivo, os membros voltam a PENDING com os campos de baixa nulos, as declarações, as transferências e a nota permanecem, e o histórico mostra REOPENED com o motivo (RN13, RN22).
  • [x] CA10. Em acerto fechado: declarar, lote, registrar e remover transferência → 409 SETTLEMENT_CLOSED; pay, update, mark_overdue, delete e restore de 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 = 1 e o membro com joined_after_close = true; declará-lo → 409 SETTLEMENT_CLOSED; reabrir, declarar e fechar de novo o baixa. O mesmo com restore de um membro removido antes do fechamento (RN14a).
  • [x] CA10b. Com devido = recebido = 6.300,00, corrigir uma declaração de PAID para UNPAID → 422 DECLARATION_BELOW_RECEIVED e nada muda; remover a transferência de 550,00, corrigir, e a falta fica 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_STALE com o billing_id e LATE_FEE_ABOVE_MAX, sem baixa nenhuma; uma UNPAID cujo vencimento foi editado para o futuro depois da declaração → 422 DECLARATION_STALE com NOT_YET_DUE (RN12).
  • [x] CA10e. Com transferências somando o devido, remover (DELETE /billings/{id}) um membro declarado PAID: o detalhe passa a falta < 0 e overpaid = true; fechar → 422 SETTLEMENT_NOT_READY; remover a transferência a mais e registrar a correta leva a READY (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 ignora FOR UPDATE e 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. pay em cobrança com channel = AGENCY_PIX → 422 AGENCY_BILLING_SETTLED_BY_AGREEMENT; pay em cobrança de boleto com payment_channel = AGENCY_PIX continua 200 (RN15).
  • [x] CA12. A rotina de atraso continua sem marcar AGENCY_PIX vencida 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 = false que 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_GLOBAL recebe 403 PLATFORM_ADMIN_CANNOT_READ_SETTLEMENTS em GET /agency-settlements, no detalhe e nos membros — mesmo com a fixture que concede todas as permissões (RN23) —, e 404 no GET /documents/{id}/download da nota (10.5).
  • [x] CA17. ANALYST, OPERATOR e VIEWER leem lista, detalhe, membros e nota (200), e recebem 403 em declarar, lote, transferência, fechar, reabrir e anexar nota (10.1).
  • [x] CA18. AGENCY_USER da Imobiliária A lê o acerto da A (200) e recebe 403 ao declarar e no lote; 404 SETTLEMENT_NOT_FOUND no detalhe, nos membros e na lista por imobiliária da B (10.4).
  • [x] CA19. AGENCY_MANAGER da A declara no acerto da A (200); recebe 404 SETTLEMENT_NOT_FOUND ao declarar no da B; 404 BILLING_NOT_IN_SETTLEMENT ao declarar, no path da A, o billing_id de uma cobrança da B; 403 em transferência, fechar, reabrir e anexar nota (10.1, 10.3, 10.4).
  • [x] CA20. GET /agency-settlements?reference_month= chamado pelo AGENCY_MANAGER da A devolve só a linha da A, numa competência em que A e B têm membros (10.4).
  • [x] CA21. ADMIN_TENANT da garantidora X recebe 404 SETTLEMENT_NOT_FOUND no detalhe de uma imobiliária da garantidora Y, e 404 BILLING_NOT_IN_SETTLEMENT ao 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_REQUIRED ao declarar (10.1).
  • [x] CA23. AGENCY_USER da B recebe 404 no GET /documents/{id}/download da nota do acerto da A; POST /documents com entity_type = "agency_settlement" → 422 INVALID_ENTITY_TYPE para qualquer papel; PATCH /documents/{id} da nota pelo AGENCY_MANAGER da A, DELETE /documents/{id} da nota pelo OPERATOR e pelo ADMIN_TENANT → 403 AGENCY_SETTLEMENT_DOCUMENT_READ_ONLY, e a nota continua lá; o AGENCY_USER da 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 o disabled (7.3).
  • [x] CA29. O AGENCY_MANAGER da 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 migration o3v5y7a9c1e3 com downgrade exercitado (upgrade → downgrade → upgrade no 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, com test_acerto_autorizacao.py antes do router
  • [x] apps/api — billing_service: extração da gravação da baixa, RN14, RN15; document_repo e document_service: RN24
  • [x] apps/api — router com CommitRoute e Swagger completo
  • [x] apps/api — make preflight e a revisão da fase
  • [x] apps/web — aba Repasses em Cobranças, tela do acerto, abas de Minha Imobiliária, diálogos; data-testid da seção 11; desktop e mobile juntos; testes unitários
  • [x] apps/web — inventário de telas no apps/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 test e make 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 um AGENCY_USER da outra parceira, não o AGENCY_MANAGER — no caminho de leitura dá no mesmo.
  • "N de M declaradas" conta em M as 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).

  • BillingResponse não ganhou partner_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 o partner_agency_id para o link Ver acerto. A lista cross-garantia ganhou a coluna no join que 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) e test_acerto_autorizacao.py (a matriz e as listas, RN19–RN22), mais o mundo compartilhado em tests/_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_REQUIRED entrou na RN7 e no contrato: a regra pedia paid_on e late_fee em PAID e 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-testid dos 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): o ConfirmDialog usa 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 PageHeader nã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-testid duplicado, 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 de documents e antes de users. Fora da cascata, a purga de uma garantidora que usou o acerto cairia inteira no Postgres pelas FKs RESTRICT. A spec não falava de purga; o apps/api/CLAUDE.md exige que entidade nova com FK RESTRICT entre 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 beforeAll e os devolve no afterAll — 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ão tests/fixtures/cobranca.ts como a seção 11 dizia: é onde moram periodo.ts e roles.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 de n2u4x6z8b0d2. Mesclada a main nesta branch (2026-10-10), a down_revision da o3v5y7a9c1e3 passou a q1e3m5a7i9l2 — senão o upgrade head acusaria 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:

  1. Só entra o que um revisor conferiu — não o que você conferiu ao escrever. O ponto é aproveitar trabalho já pago por uma rodada, não abrir atalho para afirmar sem prova.
  2. Mexeu na frase da spec, a linha sai da tabela. A afirmação volta a ser conferível. Sem isso a tabela vira o oposto do portão: uma afirmação carimbada como verdadeira porque um dia foi.

Feature sem afirmação sobre o repositório não precisa da seção — apague-a.