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.