/projects/dietbox-payment

Todos os projetos

Dietbox Payment

Assinaturas e cobrança recorrente, atrás de um checkout próprio.

Head de Tecnologia · 2023–2024

.NET 6C#CQRSVue 3VitePrimeVuePiniaCypressAzure DevOps
WebsitePrivado
Captura de tela de Dietbox Payment

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.

  1. 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.
  2. Payment serviceControllers de assinatura, transação, voucher, extensão e webhook ficam na frente dos comandos CQRS que fazem o trabalho.
  3. 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.
  4. 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.

  1. CreatedO gateway criou a assinatura do lado dele; o serviço a registra antes de existir a primeira fatura.
  2. ActivatedO primeiro pagamento da assinatura foi confirmado; o serviço marca como ativa e o acesso do cliente segue essa marcação.
  3. 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.
  4. Payment failedUma fatura da assinatura falhou ao cobrar do lado do gateway; o serviço registra a falha.
  5. SuspendedO gateway suspendeu a assinatura; o serviço espelha o estado, e o acesso segue essa marcação.
  6. ExpiredA assinatura completou seu ciclo e o gateway a encerrou; o serviço marca o registro de acordo.
  7. Invoice paidUma fatura do marketplace foi paga; o serviço registra o pagamento na subconta a que ela pertence.
  8. Invoice releasedO marketplace liberou os valores de uma fatura paga para quem detém a subconta.
  9. 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.