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:
- Contábil: divergência entre
escola_faturas.valor(SSOT da assinatura) e o valor efetivamente cobrado no boleto → relatórios de inadimplência mentem. - Conciliação MP: webhook compara
external_reference = fatura_idmas espera consistência de valor. Pagamento parcial pode marcar comopagoindevidamente. - 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)
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)
| Camada | Verificação |
|---|---|
| RLS | Apenas principal_role IN ('administrador','especialista') pode escrever boleto_valor_override_*. Gestor escola NUNCA, mesmo na própria escola. |
| Edge function | Re-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:
'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_originalriscado ao lado dovalor_overrideem 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)
| Caso | Onde |
|---|---|
| Trigger rejeita override sem motivo | tests/security/boleto-valor-override-integridade.test.ts |
| RLS rejeita gestor escola tentando override | tests/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 presente | supabase/functions/mercadopago-webhook/__tests__/override-reconciliation.test.ts |
| RTL: motivo < 20 chars bloqueia submit | src/components/admin-financeiro/__tests__/override-dialog.test.tsx |
Log pagamento.boleto_valor_override emitido com diff | supabase/functions/mercadopago-boleto/__tests__/override-log.test.ts |
3. Alternativas consideradas
| Alternativa | Por que rejeitada |
|---|---|
Permitir UPDATE direto em escola_faturas.valor | Quebra 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
| Item | Decisão |
|---|---|
| Override por linha (multa/juros separados) ou só total? | Só total — boleto_valor_override_centavos substitui o cálculo inteiro. Multa/juros breakdown deixa de ser SSOT quando há override. |
| Tamanho mínimo do motivo | 20 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.mdmem://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