Ir para o conteúdo

Blogue ·

Stripe no alojamento: quando o pagamento é seu

Bem-sucedido não é liquidado, uma disputa não é um reembolso e a cobrança é uma definição de painel. O que envolve faturar alojamento com o Stripe.

Por Provibr

Faturar alojamento não é comércio eletrónico. Uma loja recebe dinheiro uma vez e envia uma caixa; quem aloja recebe dinheiro todos os meses e mantém uma máquina a funcionar pelo meio. Essa diferença aparece num único sítio, e é o sítio que todas as integrações erram primeiro: decidir quando um pagamento é suficientemente real para se continuar a entregar.

Foi isto que encontrámos ao construir faturação recorrente sobre o Stripe. Nada disto é exótico, e quase tudo é invisível até ao mês em que um pagamento é revertido.

«Bem-sucedido» não é «liquidado»

Uma finalização de compra bem-sucedida não é dinheiro. O cartão e o iDEAL são suficientemente próximos do instantâneo para se poder fingir o contrário. O débito direto SEPA não é, e uma transferência bancária também não.

Um pagamento SEPA fica em processing depois da finalização, sem nada recebido, durante dias. Normalmente liquida. Às vezes não. Se a integração tratar a finalização como o momento da verdade, aprovisionou-se um servidor com base numa intenção.

Por isso o modelo de estados não é um booleano. Acabámos com seis fases, e exatamente uma delas quer dizer «entregar»:

FaseO que significaEntregar?
pendingIniciado, ainda nada recebidoNão
settledDinheiro recebido e nossoSim
failedNão vai acontecerNão
disputedO cliente está a contestá-loNão, mas pode voltar
reversedEstornado ou reembolsadoNão, definitivamente
unknownNão conseguimos determinarNão

Fazer com que «entregável» signifique exclusivamente settled não custa nada a escrever e dá a garantia de que nenhuma outra parte do código tem direito a uma opinião sobre o assunto. Todos os outros sítios fazem uma única pergunta.

A ordem dos eventos não é garantida, por isso torne ilegais as transições ilegais

Os webhooks chegam fora de ordem. Não é frequente, mas acontece, e a falha é silenciosa: um evento antigo sobrepõe-se a um estado mais recente, e o serviço de um cliente ou para sem motivo ou continua a correr depois de um estorno.

A defesa barata é escrever quais as transições impossíveis e recusá-las. Liquidado não pode voltar a pendente, liquidado não pode passar a falhado, pendente não pode saltar diretamente para disputado, e nada sai jamais de revertido ou de falhado. Um evento atrasado que causasse uma dessas é descartado em vez de aplicado.

A segunda defesa é arquitetural, e é a decisão que manteríamos em qualquer integração de pagamentos.

O webhook é um acelerador, não uma fonte de verdade. O nosso verifica a assinatura, remove duplicados e coloca um identificador de organização numa fila. É tudo o que faz. Depois, uma sondagem periódica obtém o estado atual junto do Stripe antes de alguma coisa mudar. A consequência vale a canalização: um webhook perdido nunca é um pagamento perdido, é um pagamento mais lento. A entrega não depende de um mecanismo de entrega que não se controla.

A verificação da assinatura tem três pormenores fáceis de errar de forma subtil:

  • Comparar em tempo constante, mas verificar primeiro os comprimentos, porque a comparação em tempo constante lança uma exceção com comprimentos diferentes em vez de devolver falso.
  • Aceitar várias assinaturas no cabeçalho. É assim que funciona a rotação do segredo, e uma implementação que leia apenas a primeira avaria-se no dia da rotação.
  • Rejeitar uma assinatura não hexadecimal antes de a interpretar como hexadecimal, porque a interpretação de hexadecimal trunca em silêncio no primeiro caráter inválido e uma assinatura de dois caracteres compara alegremente contra nada.

Também removemos duplicados ao reclamar o identificador do evento antes de publicar o aviso. Isso tem uma desvantagem conhecida: reclamar e depois falhar a publicação faz perder a pista. É aceitável precisamente porque a consulta periódica é a fonte de verdade, o que ilustra bem uma decisão arquitetural a pagar um compromisso noutro sítio.

A cobrança é uma definição do painel, não um parâmetro da API

Se um pagamento mensal falhar, alguma coisa tem de o repetir e alguma coisa tem de decidir quando o serviço para. Escrevemos essa lógica primeiro e depois apagámos quase tudo.

As Smart Retries do Stripe escolhem o momento das repetições a partir dos seus próprios dados, e aquilo em que uma subscrição se torna depois de todas as repetições falharem é configurado no painel. É uma definição de toda a conta, e não algo que se passe por subscrição, e o modo de teste e o modo real são configurados em separado, o que é uma boa forma de haver surpresas em produção depois de um teste verde.

Isso deixa do seu lado um único trabalho: traduzir o estado da subscrição naquilo que o cliente pode usar.

Estado da subscriçãoO que concedemosPorquê
active, trialingAcesso total
past_dueTolerância: um aviso, nada paradoO Stripe ainda está a repetir
unpaidSuspensoAs repetições esgotaram-se
canceledSuspenso
incompleteAcesso total, sem restriçãoO primeiro pagamento nunca foi bem-sucedido
pausedAcesso total

A linha past_due é conduzida pelo estado e não pelo relógio, e essa é a simplificação que vale a pena aceitar. A partir do momento em que o processador é dono do calendário de repetições, «há quantos dias isto está a falhar» deixa de ser uma pergunta sua. Fazê-la mesmo assim significa correr um segundo temporizador contra um calendário que não se consegue ver.

A linha incomplete é a que morde. Significa uma subscrição cujo primeiro pagamento nunca passou, o que não é a mesma coisa que um cliente pagante que deixou de pagar. Tratá-los da mesma forma suspende retroativamente um cliente em funcionamento por causa de um evento sobre uma subscrição que nunca chegou a começar.

E a suspensão é uma pausa, nunca uma eliminação. Voltar é uma ação executada por uma pessoa. Nada numa máquina de estados de faturação deve poder destruir os dados de um cliente, por mais confiante que esteja.

O defeito que devolveu o dinheiro e manteve o serviço a funcionar

Eis a falha que mais nos ensinou, e não tinha nada que ver com o fornecedor de pagamentos.

Um estorno pôs corretamente o estado de licença do cliente em bloqueado. Ao fim de cinco minutos estava outra vez em ativo. O cliente tinha o dinheiro e os servidores.

A causa era uma tarefa em segundo plano que recalculava outra coisa, o excedente de recursos, e emitia um UPDATE na mesma coluna de cinco em cinco minutos sem cláusula WHERE. Três partes de código distintas compunham o mesmo campo, cada uma correta por si, e ganhava a que corria com mais frequência.

A correção foi estrutural em vez de um remendo: uma função compõe essa coluna, recebendo três pisos como entrada, um da faturação, um do excedente, um das reversões, e mais nada a escreve. Vale a pena enunciar a regra geral em abstrato, porque se aplica muito para lá da faturação. Se mais do que um escritor calcular a mesma coluna, essa coluna não é uma decisão. É uma corrida, e resolve-se a favor da tarefa que tiver o intervalo mais curto.

Uma disputa não é um reembolso, e um reembolso parcial não é meia entrega

São duas distinções de que a faturação de alojamento precisa e que um guia genérico de pagamentos não fará por si.

Uma disputa aberta não é uma perda. Pode ser ganha. O pagamento deixa de contar como entregue, mas isso é uma afirmação sobre o pagamento e não sobre a licença: uma disputa aberta mapeia para tolerância, por isso o cliente mantém o serviço e recebe-se uma notificação. Suspender alguém que vai ganhar a disputa é uma conversa de assistência que não se quer ter. Um estorno definitivo é uma perda, e esse mapeia para suspensão.

Um reembolso parcial é o incómodo. É tentador tratá-lo como uma entrega parcial, e essa coisa não existe. Um mês de subscrição é entregue ou não é. Por isso, um reembolso parcial tira o pagamento por completo do estado liquidado, e a entrega para. É a leitura honesta, ainda que pareça dura, porque a alternativa é inventar uma fração de um serviço que o produto não consegue de facto prestar.

Quem empurrar notas de crédito para um sistema de contabilidade tem mais um aviso. No nosso, uma nota de crédito não é um recurso separado: é uma fatura com valores negativos, e a chamada duplicada copia o campo de referência do original. Essa referência é exatamente a chave em que uma verificação de idempotência procura, pelo que uma implementação ingénua encontra o original na execução seguinte, conclui que nada foi creditado e emite uma segunda nota de crédito. Dinheiro devolvido duas vezes. Todos os documentos gerados precisam da sua própria âncora, derivada do evento que os causou.

O que é aqui uma proforma, e porque é que o contador tem dois prefixos

Uma proforma é o documento que a própria plataforma produz, com o seu próprio número, e é o que o cliente vê no painel. A fatura legal é criada no sistema de contabilidade, com a sua própria série. Sem esse sistema de contabilidade ligado, a proforma é o único documento que existe, e é por isso que tem de ser numerada como deve ser em vez de tratada como um recibo.

Dois pormenores que saíram de o fazer:

O número é preenchido até quatro dígitos como mínimo, não como largura fixa. O documento dez mil fica com cinco dígitos. Truncar para manter o formato arrumado daria o mesmo número a dois documentos, e um índice único faz com que isso seja descoberto no pior momento possível.

Um contador, dois prefixos. Quando o prefixo mudou, a sequência continuou em vez de recomeçar. Dois contadores teriam produzido um documento novo cujo número difere de um antigo apenas num prefixo de duas letras, coisa que uma base de dados vê como única e uma pessoa a ler um extrato bancário não.

E um carregamento de saldo pré-pago não leva IVA na sua proforma, porque um carregamento é um pagamento antecipado e não um serviço. O IVA pertence à fatura daquilo em que o saldo for gasto. Trocar isto ao contrário significa cobrar imposto duas vezes sobre o mesmo dinheiro.

O que conseguimos medir, e o que não conseguimos

A coisa mais útil de tudo isto é o relógio de teste. Um ciclo mensal de faturação pode ser percorrido de ponta a ponta em minutos: criar um pagamento SEPA, confirmar que processing não ativa absolutamente nada, avançar o relógio para lá da janela de liquidação, ver a liquidação acontecer e verificar que a ativação ocorre exatamente uma vez e é idempotente numa segunda execução. Sem um, o ciclo mensal é a única coisa que não se consegue ensaiar, e a primeira execução real dele acontece em produção.

Dois limites honestos a tudo isto. Foi medido contra o ambiente de testes do Stripe e um webhook real entregue pela rede, e não contra uma base de clientes grande e em produção. E uma lição que se generaliza para lá dos pagamentos: um sinalizador de funcionalidade num processo e num botão não protege um esquema de base de dados. Uma vez, uma publicação levou código que lia colunas cuja migração não tinha corrido, e uma página de administração sem relação nenhuma devolveu um erro de servidor a toda a gente, porque o sinalizador estava desligado mas a consulta não.

Isto saiu da construção do lado da faturação do Provibr, onde os fornecedores de alojamento gerem a sua própria infraestrutura a partir de um painel e o agente só se liga para fora.

Comece com um host e um agente

Crie uma conta, instale o agente com um único comando e ligue o seu primeiro host Proxmox. O período experimental dura 14 dias e não pede quaisquer dados de pagamento.