Cinco lições de pagamento presencial com cartão no self-checkout

Idempotência, fila offline e o bug invisível do pinpad travado no meio da transação: lições de colocar pagamento com cartão no self-checkout.

Self-checkout é hardware fingindo ser software. Você escreve C# (porque o SDK do terminal de pagamento é C#), mas quem decide se a venda passa é um pinpad USB barato que reinicia sozinho quando a energia oscila. Este post reúne cinco lições de integrar pagamento presencial com cartão (TEF, transferência eletrônica de fundos1) no self-checkout, em várias implantações de varejo grande.

1. Idempotência é a fundação, não um detalhe

A pergunta que separa uma integração de pagamento amadora de uma séria: o que acontece quando o cliente aproxima o cartão, a transação é aprovada e o pinpad cai antes de você receber a resposta?

Resposta amadora: você trava o operador, espera ele apertar “tentar de novo” e arrisca cobrar em dobro quando a adquirente processar a primeira tentativa, que ainda estava pendente.

Resposta certa: toda chamada ao SDK leva uma chave de idempotência. Não a chave que o SDK gera. A sua chave, gerada antes da chamada. Algo assim:

var idempotencyKey = $"{store.Id}:{terminal.Id}:{transactionStartedAt:yyyyMMddHHmmss}:{Guid.NewGuid():N}";
var transaction = new Transaction
{
IdempotencyKey = idempotencyKey,
Amount = total,
StartedAt = DateTimeOffset.UtcNow,
};
await _localRepo.SaveAsync(transaction);
var result = await _tefClient.ProcessAsync(transaction);
await _localRepo.UpdateAsync(transaction.Id, result);

A chave é gravada localmente antes de chamar o pinpad. Se a chamada falha, o estado local sabe que há uma transação em voo. Quando o operador tenta de novo, você primeiro consulta na adquirente o status da transação anterior e só depois começa outra. O SDK oferece isso numa consulta de transação pendente.

2. Fila offline em SQLite, não uma List<T> na memória

A loja física não tem internet 24 horas por dia. Todo mundo sabe disso, mas o time inteiro esquece na hora de escrever a integração com o backoffice. Quando a conexão cai, a venda tem que continuar: pinpad em modo offline (com teto de valor pré-aprovado pela adquirente), e o backoffice recebe os dados quando o link volta.

A fila tem que ser persistente. Tem que sobreviver a um reboot do PDV. Tem que ser idempotente do lado de quem consome: reenviou a mesma transação, o backoffice ignora.

Use SQLite local. É embarcado, confiável, tem WAL2. Cada venda concluída vira uma linha numa tabela pending_sync com status (new | sent | acked). Um worker em segundo plano roda em loop e processa as new na ordem de criação. O endpoint do backoffice usa a chave de idempotência da venda para detectar duplicata.

Já vi loja rodar quatro dias offline depois que uma chuva forte derrubou a fibra do bairro inteiro. Quando voltou, a fila esvaziou em trinta minutos. Nenhuma venda perdida.

3. O terminal travado no meio do caminho é o seu pior bug

É o que mais aparece na sala de guerra em dia de pico. O cliente aproxima o cartão. O pinpad pisca. Pisca de novo. Mostra “AGUARDE”. E fica parado ali.

Para o SDK, a transação está em andamento. Para o operador, “travou”. Para o cliente, “já passei o cartão, cadê a maquininha?”

Nenhum comando do SDK diferencia “pinpad processando” de “pinpad travado”. Você tem que assumir que, passado um timeout (usamos 90 segundos, medidos em produção), o pinpad está num estado ruim.

A regra que adotamos: depois de 90 s sem evento, não cancelar sozinho. Mostrar ao operador a instrução de chamar um supervisor. O supervisor tem permissão para acionar o comando de cancelamento explícito do SDK. Cancelar sozinho no timeout foi nossa primeira ideia, e o jeito mais rápido de criar transação duplicada quando a adquirente processou do lado do servidor mas o pinpad nunca respondeu.

4. Log estruturado, separado do log da aplicação

O SDK de pagamento mantém o próprio log, num formato proprietário gravado pela DLL. Quando algo quebra, o suporte da adquirente pede esses arquivos. Eles não são os mesmos da sua aplicação.

Garanta que esse log seja coletado e anexado a todo chamado. Melhor ainda: leia esses logs periodicamente para achar erros recorrentes. A adquirente não vai avisar que a categoria de erro X está subindo. Você descobre quando o operador grita.

5. Treine o operador junto com o software

Esta é meta, mas é a que mais dói quando fica de fora. O operador, uma pessoa de verdade em pé no PDV, faz parte do sistema. O melhor software de pagamento do mundo não compensa um operador que não distingue “Cartão Recusado” de “Pinpad Reiniciando”. Ele vai conciliar errado e criar estoque fantasma.

Nas implantações que conduzi, dar para treinar a interface virou critério explícito. Mensagem de erro passa pelo gerente da loja antes de chegar ao código. Se o gerente diz “isso vai confundir o pessoal”, a mensagem volta para UX. Custou tempo. Poupou meses de reclamação depois da implantação.


Esse tipo de middleware não é bonito. É antigo, baseado em DLL, documentado em PDF, com um SDK que ainda usa parâmetros out nas interfaces. Mas é o sistema que processa pagamento para uma fatia relevante do varejo físico. Aprender a operar bem tem menos a ver com elegância e mais com respeitar quantos modos de falha ele esconde.

Notas

  1. TEF (transferência eletrônica de fundos) é o middleware local que orquestra a comunicação entre o PDV, o pinpad e o servidor de autorização da adquirente. Fica instalado no próprio PDV e é a espinha do pagamento presencial com cartão na maior parte do varejo físico.

  2. Write-Ahead Logging é o modo do SQLite em que as transações são gravadas num arquivo de log antes de irem para o banco principal. Sobrevive a uma queda sem corromper dados. É o padrão recomendado para PDV.