Skip to content

ADR-017 — Override Manual de Valor/Multa/Juros em Boleto (Proposed)

  • Status: Proposto (2026-06-20)
  • Categoria: Backend / Billing / Auditoria
  • Depende de: ADR-002 (Adapter), ADR-003 (Action-dispatch), ADR-006 (Logging), ADR-009 (Sanitização), ADR-011 (service_role vs RLS)

Aviso: este ADR está em estado Proposto. Nenhuma linha de código de produção deve ser escrita até que ele seja aprovado e movido para Aceito. A implementação fica gated por aceitação explícita.


1. Contexto

A entrega do Boleto Manual via UI Admin (2026-06-19, ver docs/features/billing/boleto-manual.md) deliberadamente excluiu a capacidade de o admin editar o valor cobrado (principal, multa ou juros). Hoje o admin só edita vencimento_override e o flag forcar; valor vem 100% do backend (escola_faturas.valor + configuracoes_cobranca).

Esse limite foi posto porque permitir override de valor sem trilha cria três classes de problema:

  1. Contábil: divergência entre escola_faturas.valor (SSOT da assinatura) e o valor efetivamente cobrado no boleto → relatórios de inadimplência mentem.
  2. Conciliação MP: webhook compara external_reference = fatura_id mas espera consistência de valor. Pagamento parcial pode marcar como pago indevidamente.
  3. Auditoria/LGPD: alteração de valor cobrado sem registro de quem/quando/porquê viola o princípio de auditabilidade que governa o módulo financeiro.

Existe, porém, demanda real: descontos comerciais pontuais, ajustes de erro de cobrança, acordos de regularização. Hoje o caminho é UPDATE manual no banco — o que reproduz exatamente os 3 problemas acima sem nenhuma mitigação.

2. Decisão

Quando esta capacidade for implementada, ela DEVE seguir os 7 pré-requisitos abaixo. Implementação parcial é proibida — todos os requisitos entram juntos ou nada entra.

2.1 Schema (migration única, atômica)

sql
ALTER TABLE public.escola_faturas
  ADD COLUMN boleto_valor_override_centavos BIGINT,
  ADD COLUMN boleto_valor_override_motivo TEXT,
  ADD COLUMN boleto_valor_override_por UUID REFERENCES public.usuarios(id),
  ADD COLUMN boleto_valor_override_em TIMESTAMPTZ;

-- Trigger fail-close: se override≠NULL, todos os 4 campos devem estar preenchidos
CREATE OR REPLACE FUNCTION public.fn_boleto_valor_override_integridade()
RETURNS trigger LANGUAGE plpgsql SECURITY DEFINER SET search_path = public AS $$
BEGIN
  IF NEW.boleto_valor_override_centavos IS NOT NULL THEN
    IF NEW.boleto_valor_override_motivo IS NULL
       OR length(trim(NEW.boleto_valor_override_motivo)) < 20 THEN
      RAISE EXCEPTION 'Override de valor exige motivo (mín. 20 chars)' USING ERRCODE = '23514';
    END IF;
    IF NEW.boleto_valor_override_por IS NULL THEN
      RAISE EXCEPTION 'Override de valor exige autor (boleto_valor_override_por)' USING ERRCODE = '23502';
    END IF;
    IF NEW.boleto_valor_override_em IS NULL THEN
      NEW.boleto_valor_override_em := now();
    END IF;
  END IF;
  RETURN NEW;
END;
$$;

2.2 Autorização (fail-close em 3 camadas)

CamadaVerificação
RLSApenas principal_role IN ('administrador','especialista') pode escrever boleto_valor_override_*. Gestor escola NUNCA, mesmo na própria escola.
Edge functionRe-valida via JWT — ignora override do payload se role insuficiente, sem erro silencioso (responde 403).
Trigger DBÚltima linha de defesa: bloqueia INSERT/UPDATE de override sem motivo válido.

2.3 Log canônico novo

Adicionar em src/constants/log-actions.ts:

ts
'pagamento.boleto_valor_override'  // diff {valor_antes, valor_depois, motivo, autor}

Cobertura obrigatória em LOG_ACTION_OPTIONS (SSOT do filtro admin) — ver mem://architecture/log-actions-catalog-ssot.

2.4 UI

  • Campo "motivo" obrigatório, mín. 20 chars, textarea (não input single-line).
  • Modal de segunda confirmação após o primeiro submit: "Você está cobrando R$ X em vez de R$ Y. Motivo: '...'. Confirmar?"
  • Badge visual permanente na linha da fatura: Valor ajustado.
  • Exibir valor_original riscado ao lado do valor_override em todas as superfícies admin.

2.5 PDF do boleto

Rodapé obrigatório: "Valor ajustado por <nome_admin> em <data_BRT>. Motivo: <motivo>." — exigência de auditoria. Sem isso, o boleto impresso não tem rastreabilidade da alteração.

2.6 Conciliação webhook

mercadopago-webhook DEVE comparar valor_pago contra boleto_valor_override_centavos quando presente (não contra escola_faturas.valor). Caso contrário, pagamento override vira "pago parcial" incorretamente.

2.7 Cobertura de testes (mínima para aprovar)

CasoOnde
Trigger rejeita override sem motivotests/security/boleto-valor-override-integridade.test.ts
RLS rejeita gestor escola tentando overridetests/security/idor-boleto-valor-override.test.ts
Edge fn ignora override de role insuficiente (403)supabase/functions/mercadopago-boleto/__tests__/override-authz.test.ts
Webhook reconcilia contra override quando presentesupabase/functions/mercadopago-webhook/__tests__/override-reconciliation.test.ts
RTL: motivo < 20 chars bloqueia submitsrc/components/admin-financeiro/__tests__/override-dialog.test.tsx
Log pagamento.boleto_valor_override emitido com diffsupabase/functions/mercadopago-boleto/__tests__/override-log.test.ts

3. Alternativas consideradas

AlternativaPor que rejeitada
Permitir UPDATE direto em escola_faturas.valorQuebra SSOT da assinatura; recálculo retroativo de relatórios; sem trilha de motivo.
Coluna desconto_centavos apenas (sem motivo/autor)Resolve metade do problema contábil mas zero do problema de auditoria. Atrai uso indiscriminado.
Fluxo de "nota de crédito" separada (NF-like)Mais correto contábil, mas complexidade muito acima do necessário para o caso de uso (desconto pontual). Pode ser v2 deste ADR.

4. Consequências

Positivas

  • Caso de uso real (desconto comercial, ajuste de erro) sai do "UPDATE manual no banco" e ganha trilha.
  • Webhook continua reconciliando corretamente.
  • Auditoria LGPD/contábil tem quem/quando/porquê em todo override.

Negativas

  • 4 colunas novas em escola_faturas (já é tabela com 41 colunas — alta carga cognitiva).
  • Trigger DB adiciona latência marginal a todo UPDATE em escola_faturas.
  • UI ganha estado extra (override badge, valor riscado) — risco de poluir lista de faturas.

5. Trade-offs

ItemDecisão
Override por linha (multa/juros separados) ou só total?Só totalboleto_valor_override_centavos substitui o cálculo inteiro. Multa/juros breakdown deixa de ser SSOT quando há override.
Tamanho mínimo do motivo20 chars (suficiente para "Desconto acordo cliente X")
Permitir override em fatura pago?Não — fail-close. Override só antes da compensação.
Histórico de overrides (múltiplos por fatura)?Fora deste ADR — depende de escola_fatura_boleto_historico (item 3f do roadmap de boleto-manual).

6. Conformidade

Esta seção será expandida quando o ADR for movido para Aceito. Checklist preliminar:

  • [ ] Migration atômica com os 4 campos + trigger.
  • [ ] RLS policy explícita para override (não herdar da policy genérica de escola_faturas).
  • [ ] Edge fn revalida JWT antes de aplicar override.
  • [ ] UI bloqueia submit com motivo < 20 chars.
  • [ ] PDF tem rodapé de override.
  • [ ] Webhook reconcilia contra override.
  • [ ] Os 6 testes da §2.7 verdes.

7. Débito técnico

Nenhum — feature ainda não existe.

8. Referências

  • docs/features/billing/boleto-manual.md §"Fora de escopo (rastreado em ADR-017)"
  • docs/features/billing/encargos-atraso.md
  • mem://features/billing/boleto-manual-ui-backlog
  • ADR-006 (Logging Fire-and-Forget) — para pagamento.boleto_valor_override
  • ADR-009 (Sanitização de Erro 2 Camadas) — mensagem de erro 403 amigável