Blog

Conciliação financeira automatizada: o que ninguém conta.

Toda empresa que processa pagamento tem alguém conferindo se o dinheiro que entrou é o dinheiro que deveria ter entrado. Na maioria das vezes esse alguém usa planilha.

A automatização parece direta: baixa o arquivo do gateway, compara com a sua tabela, aponta as diferenças. Você faz isso numa tarde e descobre trezentas divergências.

Aí começa o trabalho de verdade, que é entender que a maioria delas é normal.

Por que nunca bate exatamente

Quatro fontes de divergência legítima, que existem em qualquer integração.

Defasagem de tempo. Uma transação às 23h58 no seu fuso pode cair no dia seguinte no relatório do gateway. Não é erro, é corte de janela. Qualquer conciliação que compare “o dia de ontem” contra “o dia de ontem” vai acusar isso todo dia.

Estados intermediários. Você registra a autorização, o gateway reporta a captura. São eventos diferentes, em momentos diferentes, e um pode existir sem o outro por horas ou dias.

Taxa e arredondamento. O valor bruto é seu, o líquido é o que o gateway repassa. A taxa às vezes tem centavo de arredondamento que o cálculo deles faz diferente do seu.

Eventos que você não gerou. Chargeback, estorno iniciado no painel do gateway, ajuste manual do suporte deles. Aparecem no relatório e não têm origem no seu sistema.

Um sistema de conciliação que trata tudo isso como erro produz trezentos alertas por dia, e em duas semanas ninguém olha mais. Alerta que ninguém olha é pior que alerta nenhum, porque cria a ilusão de cobertura.

O desenho que funciona

Três etapas, e a terceira é a que quase todo mundo esquece.

Normalizar. Traga os dois lados pra mesma forma antes de comparar. Mesmo fuso, mesma unidade (centavos inteiros), mesma chave de casamento.

A chave é a decisão mais importante. O ID do gateway é o melhor candidato, desde que você o tenha gravado na hora, que é uma daquelas coisas óbvias que só se percebe quando falta.

create table payment (
  id                uuid primary key,
  provider          text not null,
  provider_txn_id   text,              -- guarde SEMPRE, mesmo sem uso imediato
  amount_minor      bigint not null,
  fee_minor         bigint,
  status            text not null,
  authorized_at     timestamptz,
  captured_at       timestamptz,
  unique (provider, provider_txn_id)
);

Casar. Casamento exato pelo ID primeiro. O que sobrar, tenta casamento por heurística: valor mais data mais últimos dígitos do cartão, dentro de uma janela. E o que sobrar depois disso é o que interessa.

Classificar. É aqui que a conciliação vira útil. Cada divergência recebe uma categoria e um destino.

type Divergence =
  | { kind: "aguardando_janela"; action: "ignorar_ate"; until: Date }
  | { kind: "taxa_diferente";    action: "ajuste_automatico" }
  | { kind: "so_no_gateway";     action: "investigar" }
  | { kind: "so_no_sistema";     action: "investigar_urgente" }
  | { kind: "valor_divergente";  action: "bloquear_e_alertar" };

Só as duas últimas categorias acordam alguém. As outras seguem por caminho automático ou entram numa fila que se resolve sozinha na próxima execução.

As duas categorias que importam

Só no gateway. Existe uma cobrança lá que você não tem aqui. Quase sempre é webhook perdido. O cliente pagou e o seu sistema não sabe: ele não recebeu o produto, e vai reclamar.

Essa categoria é a que justifica a conciliação existir. É a rede que pega o que a integração deixou passar, e ela deveria disparar reprocessamento automático a partir do dado do gateway.

Só no sistema. Você registrou uma cobrança que o gateway não conhece. Mais raro e mais grave, porque significa que você deu algo em troca de dinheiro que não entrou.

Se isso aparece com frequência, tem bug de fluxo: alguém está marcando pagamento como aprovado antes da confirmação.

O erro de desenho mais comum

Conciliar contra a tabela de pedidos em vez de contra o ledger.

A tabela de pedido tem o estado atual. Ela não tem estorno parcial, não tem ajuste, não tem a taxa separada do bruto. Você acaba conciliando um número que já é resultado de várias operações, e quando não bate não dá pra saber qual delas causou.

O ledger tem cada movimento. A conciliação passa a comparar movimento contra movimento, e a divergência aponta pra transação específica.

O que gravar de cada execução

Conciliação sem histórico é diagnóstico sem prontuário.

create table reconciliation_run (
  id             uuid primary key,
  provider       text not null,
  period_start   timestamptz not null,
  period_end     timestamptz not null,
  matched_count  int not null,
  divergences    jsonb not null,
  total_provider_minor bigint not null,
  total_internal_minor bigint not null,
  created_at     timestamptz not null default now()
);

Isso permite responder “desde quando essa divergência existe?”, que é a primeira pergunta que alguém faz e a mais difícil de responder sem registro.

O ganho que não é financeiro

Conciliação bem feita é o detector de bug mais eficiente que um sistema de pagamento tem.

Ela não conhece a sua lógica, não sabe suas premissas, e por isso pega erro de premissa: aquele em que o sistema está internamente coerente e errado em relação ao mundo. Nenhum teste unitário pega isso, porque o teste foi escrito pela mesma pessoa que escreveu a premissa errada.

A primeira vez que rodar, prepare-se: vai aparecer coisa de meses atrás. Isso é desconfortável e é exatamente o retorno do investimento.

Sobre a base contábil que sustenta essa comparação, escrevi em partidas dobradas para desenvolvedor.

← Voltar pro blog