# Nunca pagar duas vezes: como deixamos o dinheiro do motorista à prova de falhas

> Repasses, saques e estornos passam por redes que falham na pior hora. Os padrões que usamos para que cada real saia uma vez só, mesmo quando a resposta do banco se perde.

- Endereço: https://engenharia.cariocataxi.com.br/dinheiro-sem-pagar-duas-vezes/
- Publicado em: 2026-11-10
- Autoria: Jacques Ben Bilombe (Desenvolvedor, Carioca Taxi) — https://www.linkedin.com/in/jacques-ben-bilombe/
- Categorias: Pagamentos, Backend
- Série: Construindo o Carioca Taxi

Num app de corridas, o dinheiro anda em várias etapas. O passageiro paga a corrida pelo app, por Pix ou cartão. O valor
entra na conta do Carioca Taxi. Quando a corrida termina, a parte do motorista (o valor menos a comissão) é repassada
para a conta de recebimento dele, e o motorista saca para o banco quando quiser. Se a corrida é estornada, a parte dele
volta.

Cada seta desse caminho é uma chamada de rede para uma instituição de pagamento. E chamadas de rede falham de um jeito
traiçoeiro: às vezes o pedido chega, o dinheiro se move, e **a resposta se perde no caminho de volta**. Para quem está
do lado de cá, um "deu certo" perdido parece igual a um "deu errado". Se tentarmos de novo, pagamos duas vezes.

Este é o segundo artigo da série. Aqui contamos os padrões que usamos para que isso não aconteça.

## O princípio: atrasar é aceitável, duplicar não é

Toda decisão do código de pagamentos segue uma regra: **na dúvida, não mexa no dinheiro agora**. Um repasse que sai um
minuto depois, no próximo ciclo, é um incômodo pequeno. Um repasse em dobro é um prejuízo, e um problema com o motorista
para desfazer.

## 1. Cada movimento tem uma identidade

Antes de qualquer transferência existir no banco de pagamentos, ela existe no nosso banco, como uma linha com
identidade própria. Um repasse nasce ligado à corrida e ao tipo ("corrida", "gorjeta", "taxa de cancelamento"), e um
índice único garante **um repasse por corrida e tipo**. O cron pode rodar dez vezes ao mesmo tempo que a mesma corrida
continua com um repasse só.

Esse identificador vai junto como **referência** em cada chamada ao gateway (`repasse-<id>`, `saque-<id>`,
`devolucao-<id>`). É ele que nos permite, depois, perguntar: "essa transferência já existe?"

## 2. Reservar antes de transferir

Dois caminhos podem tentar enviar o mesmo repasse: o fim da corrida e o cron. Então, antes de chamar o gateway, o código
**reserva** a linha, mudando o estado de "agendado" para "enviado" só se ela ainda estiver agendada. Quem não conseguir
reservar desiste.

![Figura 1: Os estados de um repasse. Toda volta para trás passa por uma conferência no gateway antes de tentar de novo.](https://engenharia.cariocataxi.com.br/figuras/repasse-estados.svg)

Junto com a reserva, gravamos a hora (`enviado_em`). Ela resolve um caso que demoramos a ver. E se o servidor cair
**entre** reservar e receber a resposta? A linha fica "enviada" para sempre, sem número de transferência. Agora o cron
procura esses casos depois de cinco minutos e decide pelo que encontrar no gateway.

## 3. A resposta se perdeu? Pergunte antes de repetir

É o coração do problema. Quando a chamada de transferência dá erro, nós **não** marcamos simplesmente "falhou". Primeiro
perguntamos ao gateway se existe uma transferência com a nossa referência. Se existe, ela deu certo e só a resposta se
perdeu: gravamos como feita.

![Figura 2: Transferir sem duplicar. A resposta perdida é recuperada pela referência, não por uma nova transferência.](https://engenharia.cariocataxi.com.br/figuras/transferencia-sem-duplicar.svg)

Há um detalhe. A documentação do gateway que usamos não permite filtrar transferências pela referência, só por data.
Então listamos as transferências criadas a partir da véspera e procuramos a nossa. E, se essa busca também falhar, não
transferimos: o repasse fica para o próximo ciclo.

```ts
async function transferirSemDuplicar(gw, referencia, desde, jaTentou, transferir) {
  if (jaTentou) {
    const existente = await gw.buscarTransferencia(referencia, desde); // se falhar, lança: não transfere
    if (existente) return existente;
  }
  try {
    return await transferir();
  } catch (erro) {
    const existente = await gw.buscarTransferencia(referencia, desde).catch(() => null);
    if (existente) return existente; // deu certo; só a resposta se perdeu
    throw erro;
  }
}
```

A mesma função envolve os três movimentos: o repasse ao motorista, a devolução num estorno e o saque.

## 4. Dois caminhos, uma marca

Mesmo com o repasse automático, o painel tem um "repassar à mão", para os casos em que a equipe paga direto. Dois
caminhos para o mesmo dinheiro são um convite à duplicata. Por isso os dois deixam **a mesma marca** na corrida
("repassada em"):

- o automático não envia o que já tem marca;
- o manual ignora corridas com uma transferência a caminho, já feita ou presa num saque.

Encontramos essa última regra numa revisão interna, antes de ligar o repasse automático: o painel teria pago de novo
uma corrida cuja transferência ainda estava a caminho. Um teste agora prova que não acontece.

## 5. Quando o banco diz "ainda não"

Instituições de pagamento abrem a conta de recebimento de cada motorista com verificação de identidade, e no começo
costumam limitar quantas contas novas uma plataforma pode abrir. Um motorista que entra nesse intervalo não pode ficar
sem receber. Então:

- ele entra numa **fila**, e o cron tenta abrir a conta de novo, por ordem de chegada, sem martelar o gateway (para no
  primeiro que ainda for recusado);
- o que ele ganha fica **guardado** na conta do Carioca Taxi, como repasses agendados;
- ele pode pedir um **saque expresso**: a equipe transfere pelo painel e dá baixa. Os repasses incluídos ficam presos
  àquele saque, para não irem também para a conta dele se ela abrir no meio do caminho. E uma corrida estornada antes do
  pagamento sai do valor sozinha.

## 6. O dinheiro do cartão tem prazo

Pix cai na hora; cartão, não. O gateway libera o valor de uma corrida paga no cartão semanas depois. Se repassarmos ao
motorista antes disso, estamos pagando com o dinheiro de outras corridas. Cada repasse nasce com uma data de
"disponível em" que segue o prazo do cartão, e o app do motorista mostra quanto ainda está "a liberar" e quando. A
taxa de cancelamento paga com cartão segue a mesma regra.

## Como testamos

O gateway tem uma versão **simulada**, com a mesma interface do real, e ela sabe falhar de propósito. Num teste, ela
aceita a transferência e "perde" a resposta. Em outro, a busca fica fora do ar. Em outro, o servidor "cai" entre a
reserva e a chamada. Cada caso tem um teste que confere o resultado que importa: **o saldo do motorista no fim**.
Contar quantas vezes uma função foi chamada não basta.

O adaptador do gateway real é testado contra respostas montadas a partir da documentação oficial, campo a campo.

## Considerações finais

Nada aqui é novo: identidade para cada movimento, reserva antes de agir, conferência antes de repetir, uma marca só
para dois caminhos. São os mesmos princípios de qualquer sistema financeiro. O trabalho está em aplicá-los a **cada**
seta do caminho do dinheiro, inclusive às que parecem inofensivas, como um botão de "repassar à mão" num painel.

No próximo artigo, saímos do servidor e vamos para o painel do táxi: GPS em segundo plano no Android.
