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.