/projects/dietbox-payment
← Todos os projetosDietbox Payment
Assinaturas e cobrança recorrente, atrás de um checkout próprio.
Head de Tecnologia · 2023–2024

Visão geral
O serviço responsável pelo dinheiro: comandos de assinatura de um lado, um handler de webhook para cada evento que um gateway de pagamento emite do outro, e duas integrações de gateway no meio — Iugu e TSPay, cada uma no próprio pacote transversal. Ele é mantido separado porque dinheiro tem um modo de falha diferente do resto do produto — trem de release próprio, num repositório que compartilha com os outros serviços da plataforma.
O que eu fiz
Como arquiteto principal do conjunto, defini os padrões sobre os quais este serviço é construído: o pipeline de release com filtro de caminho que permite que ele suba no próprio trem, e a convenção de pacote transversal em que toda integração de terceiro é embrulhada antes de um serviço depender dela. Os comandos, os handlers de webhook e o próprio checkout foram escritos pelo time.
- O filtro de caminho do pipeline de release, para que um hotfix de pagamento suba pelo próprio branch sem redeployar os outros quatro serviços.
- O pacote transversal em que cada integração de gateway vive, e o único projeto compartilhado que os traz para qualquer serviço que precise deles.
- O ambiente Azure onde este serviço é publicado, configurado da mesma forma que seus vizinhos.
Os comandos de assinatura, os handlers de webhook e o cliente de checkout foram trabalho de um time: o autor tem aproximadamente um décimo dos commits do cliente de checkout, entre fevereiro de 2023 e julho de 2024, e cerca de um quinto dos do serviço, cujo repositório só começa em outubro de 2023 — a maior parte dos dois pertence a outros engenheiros.
O problema
Uma assinatura não mora só no banco de dados do próprio produto — ela também mora em qualquer gateway que esteja processando, e a opinião desse gateway sobre o estado da assinatura chega de forma assíncrona, por webhook, no tempo dele. Dois gateways ficaram ativos ao mesmo tempo enquanto os assinantes eram migrados de um para o outro, cada um com seus próprios nomes de evento, seu próprio formato de payload e sua própria ideia do que é uma assinatura. E cada entrega de webhook precisa ser reconciliada com o que o produto já acredita ter acontecido, não simplesmente aceita como verdade.
Arquitetura
Um checkout em Vue na frente, comandos e controllers CQRS no meio, e duas integrações de gateway, cada uma no próprio pacote — com cada gateway fechando o ciclo de forma assíncrona por um endpoint de webhook próprio.
- Checkout clientO checkout em Vue — telas de assinatura, renovação e páginas de agradecimento — chama os comandos do serviço: assinar, criar fatura, gerar link de pagamento.
- Payment serviceControllers de assinatura, transação, voucher, extensão e webhook ficam na frente dos comandos CQRS que fazem o trabalho.
- Gateway packagesIugu e TSPay vivem cada um em um pacote transversal com interface própria, alcançados pelo único projeto compartilhado que todo serviço deste repositório referencia.
- Gateway webhooksCada gateway publica de volta a própria opinião sobre a assinatura num endpoint próprio, onde uma fábrica mapeia os nomes de evento daquele gateway em comandos — um diretório de handler por evento.
O ciclo de vida da assinatura
Cada etapa abaixo é um diretório na árvore de handlers de webhook, nomeado pelo evento do gateway que ela responde.
- CreatedO gateway criou a assinatura do lado dele; o serviço a registra antes de existir a primeira fatura.
- ActivatedO primeiro pagamento da assinatura foi confirmado; o serviço marca como ativa e o acesso do cliente segue essa marcação.
- ChangedUm plano, um preço ou uma forma de pagamento mudou do lado do gateway; o serviço atualiza seu próprio registro para acompanhar.
- Payment failedUma fatura da assinatura falhou ao cobrar do lado do gateway; o serviço registra a falha.
- SuspendedO gateway suspendeu a assinatura; o serviço espelha o estado, e o acesso segue essa marcação.
- ExpiredA assinatura completou seu ciclo e o gateway a encerrou; o serviço marca o registro de acordo.
- Invoice paidUma fatura do marketplace foi paga; o serviço registra o pagamento na subconta a que ela pertence.
- Invoice releasedO marketplace liberou os valores de uma fatura paga para quem detém a subconta.
- Invoice refundedUma fatura do marketplace foi reembolsada; o serviço reverte o que havia registrado na subconta.
O que faz
- Assinatura e renovação, com um caminho de suspensão quando um pagamento falha.
- Vouchers e extensões de plano, ajustando uma assinatura sem cancelar e recriar.
- Links de pagamento gerados sob demanda, para uma cobrança fora do fluxo normal de checkout.
- Subcontas de marketplace, com seus próprios eventos de fatura paga, liberada e reembolsada.
Decisões de engenharia
Um repositório, cinco trens de release
O serviço de pagamento compartilha o repositório com os serviços core, de autenticação, de alimentos e de jobs, e cada um dos cinco sobe no próprio trem de release: arquivo de pipeline próprio, gatilho de branch próprio, e um filtro de caminho que nomeia os diretórios dos outros quatro serviços como motivo para não construir. Um hotfix de pagamento não redeploya a autenticação. Um monorepo sem deploy compartilhado.
Um pacote por gateway, não uma interface para todos
Iugu e TSPay não compartilham interface — não têm nada em comum para compartilhar. Cada um fica no próprio pacote transversal, com vocabulário próprio, endpoint de webhook próprio e fábrica de comandos própria, e um handler pede pelo nome o gateway de que realmente precisa. A qual gateway uma assinatura pertence é um valor do domínio, não um detalhe escondido dele, e foi isso que permitiu mover os assinantes de um para o outro um a um: um webhook do TSPay ainda consegue chegar ao Iugu para suspender a assinatura antiga de uma nutricionista que acabou de ser migrada.
A árvore de webhooks é a máquina de estados
Existe um handler por evento do gateway, nomeado pelo próprio evento — assinatura criada, fatura paga, fatura reembolsada — em vez de um único endpoint que decide com base num campo do payload. A estrutura de diretórios é o ciclo de vida, legível sem abrir um único arquivo.
Um checkout que não é o aplicativo
O funil de compra sobe como um cliente próprio — seu próprio app em Vue, sua própria suíte de Cypress reportando via Allure — separado do resto do produto, no próprio ritmo.