Blog

Ledger imutável: como corrigir um erro sem apagar nada.

A objeção mais comum a um ledger imutável é prática: e quando a gente errar?

Errar acontece. Taxa calculada com o percentual antigo, valor invertido, cobrança lançada na conta errada, integração que mandou o dobro. Se a tabela não aceita UPDATE nem DELETE, o que se faz?

Se faz o que contador faz desde antes de existir computador. Registra a correção.

Os dois mecanismos

Estorno. Um lançamento com o valor invertido, que anula o efeito do original.

Original (errado):
  caixa_gateway         +25000
  conta_cliente         -25000

Estorno:
  caixa_gateway         -25000
  conta_cliente         +25000

Lançamento correto:
  caixa_gateway         +19900
  conta_cliente         -19900

O saldo final está certo. As três transações continuam visíveis. Qualquer pessoa que olhe o extrato entende o que aconteceu: cobrou 250, estornou, cobrou 199.

Ajuste. Um lançamento só, com a diferença.

Ajuste:
  caixa_gateway         -5100
  conta_cliente         +5100

Menos linhas, mesmo saldo. E o extrato fica mais difícil de ler, porque aparece um valor que não corresponde a nenhuma operação de negócio.

Minha regra: estorno quando o cliente vê o extrato, ajuste quando é conta interna. O cliente entende “cobrança e estorno”. Ele não entende um lançamento de R$ 51 que ninguém sabe explicar.

O elo entre os três

Correção sem vínculo com o original é ruído. Cada transação precisa apontar pra que ela corrige.

alter table ledger_transaction
  add column corrects_transaction_id uuid references ledger_transaction(id),
  add column correction_reason text,
  add column corrected_by uuid references users(id);

Com isso, três perguntas ficam respondíveis por consulta:

Essa transação foi corrigida depois? Existe alguma linha apontando pra ela.

Essa transação é uma correção? Ela aponta pra outra.

Quem autorizou, quando e por quê.

Sem esse vínculo, o histórico tem a informação e ninguém consegue extrair. Que é quase a mesma coisa que não ter.

A correção também é uma operação de negócio

Corrigir ledger não é manutenção técnica. É ato que muda dinheiro de lugar.

Isso implica algumas coisas que costumam ser esquecidas.

Precisa de autorização. Nem todo mundo que consegue rodar um script deveria conseguir corrigir lançamento. É permissão específica, separada de “acesso ao banco”.

Precisa de motivo escrito, em texto livre e obrigatório. Seis meses depois, correction_reason é a única coisa que explica.

Precisa de limite. Correção acima de determinado valor exige segunda pessoa. É controle chato e é o que impede que um erro operacional vire um problema maior.

E precisa entrar no audit trail como evento próprio, não como consequência.

O que fazer com dez mil lançamentos errados

Erro individual é fácil. O caso difícil é o sistemático: um bug rodou por três semanas e produziu milhares de lançamentos errados.

A tentação é gigantesca: UPDATE ... WHERE created_at BETWEEN. Uma linha resolve.

Resolve e destrói a propriedade que faz o ledger valer alguma coisa. Depois disso, ninguém consegue mais afirmar que o histórico reflete o que aconteceu, porque houve pelo menos uma exceção. E onde houve uma, ninguém sabe se houve outras.

O caminho é gerar as correções em lote, com o mesmo mecanismo das individuais:

const affected = await findAffectedTransactions(bugWindow);

for (const original of affected) {
  await ledger.correct({
    correctsTransactionId: original.id,
    entries: invert(original.entries),
    reason: "Correção em lote — bug de cálculo de taxa (INC-2026-0142)",
    correctedBy: SYSTEM_CORRECTION_USER,
  });
  await ledger.post(recalculate(original));
}

Fica mais lento, gera muito mais linha, e a tabela cresce. Aceitável. O que não é aceitável é ter um ledger em que a resposta pra “esse número está certo?” seja “depende de quem mexeu”.

Um detalhe prático: rode em lote pequeno, com controle de retomada, e registre o número do incidente no motivo. Correção em massa que falha no meio e é reiniciada sem controle produz correção duplicada, o que é pior que o erro original.

Data do fato e data do registro

Correção tem duas datas, e confundir gera relatório errado.

occurred_at é quando o fato ocorreu, que para uma correção é o momento da correção, não o do erro. effective_date é o período contábil a que ela se refere, que pode ser o mês passado.

Isso importa quando o fechamento já aconteceu. Corrigir maio em julho pode exigir reabertura do período ou lançamento no período atual com referência ao anterior, dependendo da regra contábil. Essa decisão é do financeiro, não da engenharia, e precisa estar respondida antes de existir o primeiro caso.

O ganho que aparece depois

Um ledger com histórico completo de erros e correções parece pior que um ledger só com números certos.

É o contrário. Quando alguém questiona um valor, você mostra a sequência inteira: o que foi lançado, quando se percebeu, quem corrigiu e por quê. Isso encerra a conversa.

Num ledger que aceita UPDATE, a mesma pergunta não tem resposta. O número está lá, sozinho. E “confia em mim” não é resposta quando tem dinheiro no meio.

Sobre a estrutura que sustenta isso, escrevi em como funciona um core ledger por dentro.

← Voltar pro blog