Em produção
Trips do Geo
Uma plataforma de reservas para expedições guiadas com vagas limitadas por saída, feita para acertar reservas simultâneas e os webhooks de pagamento.
Ver no ar Abre em nova abaO problema
Uma saída com 20 vagas pode esgotar em minutos, e duas pessoas podem tentar reservar a última vaga na mesma fração de segundo — o fluxo de reserva precisa acertar isso sempre, não na maioria das vezes.
O que construí
Uma loja em Next.js e uma API em NestJS onde toda operação que mexe em capacidade — reserva, cancelamento em grupo, reembolso — roda dentro de uma única transação que trava a saída, e a reserva quando ela já existe, antes de mudar qualquer coisa.


Decisões técnicas
As vagas não são controladas por uma coluna contadora — elas são contadas na hora, dentro de uma transação que trava a linha da saída antes de checar a capacidade e antes de criar a reserva. Duas pessoas reservando a última vaga no mesmo instante batem nessa mesma trava; uma consegue a vaga, a outra recebe um “esgotado” claro em vez de uma corrida que superlota a saída. Tenho um teste de integração que prova isso diretamente: ele dispara duas reservas simultâneas para uma saída com 3 vagas, cada uma pedindo 2 pessoas, e confere que só uma tem sucesso.
Uma reserva não passa a ocupar vaga só depois de paga — ela conta a partir do momento em que é criada, por 30 minutos, tenha o pagamento chegado ou não. Essa espera é o que impede o intervalo entre “reservado” e “pago” de virar uma segunda forma de superlotar: quem está no meio do checkout ainda segura a vaga, mas se abandonar, ela se libera sozinha assim que o prazo expira.
Uma reserva pode ter até 20 pessoas. Quando alguém de um grupo já pago desiste, cancelar essa pessoa é um cancelamento lógico, não uma exclusão — libera a vaga dela na hora, mas o resto do grupo mantém as vagas e o pagamento intocados. A cota de reembolso dela é calculada pelo preço da própria pessoa, não dividida igualmente, e vai para um total de reembolso pendente separado, não para o sinalizador de reembolso da reserva inteira — reembolso nunca sai automático aqui; um humano confirma.
O webhook do Mercado Pago trava a linha da reserva alvo antes de mexer nela, confere a assinatura da requisição, e trata um ID de pagamento já visto como algo que não faz nada de novo. Um segundo pagamento caindo numa reserva já paga não é descartado nem contado duas vezes — ele vai para uma fila de reembolso manual e um e-mail é disparado, não para o cliente, mas para o guia responsável pela saída, já com o WhatsApp do cliente ali na mensagem para o guia entrar em contato direto. Reembolsar dinheiro automaticamente via API é exatamente o tipo de decisão que não quero que um webhook tome sozinho — um humano confirma primeiro.
As duas corridas que realmente importam se resumem a uma notificação de pagamento chegando depois que outra coisa já mudou. Se chega depois que o prazo expirou e a vaga foi para outra pessoa, a reserva é marcada como expirada e sinalizada para reembolso em vez de confirmada numa superlotação. Se chega enquanto a própria saída está sendo cancelada, o cancelamento vence e a reserva inteira é sinalizada para reembolso — a ordem em que essas duas checagens rodam é proposital, e é a diferença entre reembolsar uma reserva e reembolsar uma saída inteira por engano.
Resultado
Em produção em tripsdogeo.com.br, com um teste de integração que dispara duas reservas simultâneas para a última vaga e confirma que só uma é aceita.
Stack
- NestJS
- Prisma
- PostgreSQL
- Next.js
- TypeScript
- Mercado Pago