surfaai · parte 6 de 7
Migração de gateway em produção: Asaas → Stripe sem quebrar o checkout
07 de outubro de 20267 min de leituraCesar Eduardo Sturmer
Os artigos 1 e 2 desta série prometeram esta história duas vezes: o surfaai não nasceu no Stripe. Começou no Asaas, e eu troquei o gateway com a plataforma já em uso.
As migrations contam o calendário melhor que a minha memória. As tabelas do Asaas entraram em 27 de janeiro. Os campos do Stripe entraram em 3 de março — quatro migrations no mesmo dia. A limpeza do que sobrou é de 6 de março. Cinco semanas de Asaas, e a troca concentrada em três dias.
O título diz "sem quebrar o checkout" e não "sem quebrar nada" por um motivo que eu explico no fim: uma parte quebrou, de propósito.
Por que essa migração é diferente das outras
Trocar biblioteca de UI, ORM, até banco de dados: todas essas têm uma rede de segurança. Você testa à exaustão e, no pior caso, reverte o deploy.
Gateway de pagamento não tem isso, porque o estado que você está migrando não vive só no seu banco. Ele vive em três lugares ao mesmo tempo:
- no seu banco — pagamentos, créditos, status
- no gateway antigo — que continua sendo a verdade sobre o histórico dele
- na cabeça do usuário — ele já pagou, já tem crédito, e espera que aquele crédito continue valendo amanhã
Reverter um deploy conserta o primeiro. Não conserta os outros dois. Se eu quebrasse o crédito de alguém que pagou em fevereiro, nenhum git revert devolveria a confiança.
Decisão 1: não reescrever dado histórico
A primeira tentação é normalizar tudo — pegar cada pagamento do Asaas e encaixar nos campos que o Stripe usa, para ter um schema só. Eu não fiz isso, e é a decisão da qual tenho mais certeza.
Transformar dado financeiro histórico para caber num formato que não existia na época da transação é a receita da inconsistência sutil: um campo que quase bate, um status sem equivalente direto, um valor de taxa calculado por uma regra que não era a regra daquele dia. E o pior é que você não descobre na hora. Descobre seis meses depois, conciliando, sem saber mais qual dos dois números era o certo.
Então os registros antigos continuaram com o formato antigo. Um registro de fevereiro nunca finge ser um registro de março. O custo é um código de leitura que sabe lidar com dois formatos. Vale muito menos que o risco de corromper histórico de dinheiro.
Decisão 2: separar o renomear do integrar — e eu fiz na ordem errada
Aqui está o erro que eu cometi, e ele está documentado na própria migration.
Três colunas guardavam identificadores de pagamento com nome do gateway velho. A migration 20260303000004 renomeou as três:
-- payments: asaas_payment_id → gateway_payment_id
ALTER TABLE payments
RENAME COLUMN asaas_payment_id TO gateway_payment_id;
-- payments: asaas_fee → gateway_fee
ALTER TABLE payments
RENAME COLUMN asaas_fee TO gateway_fee;
-- provider_payment_settings: asaas_fee_paid_by → gateway_fee_paid_by
ALTER TABLE provider_payment_settings
RENAME COLUMN asaas_fee_paid_by TO gateway_fee_paid_by;Nomes neutros, sem marca de fornecedor — gateway_payment_id guarda um pi_xxx do Stripe hoje e guardaria outro amanhã. Isso está certo.
O problema é o comentário no cabeçalho dessa migration, escrito por mim:
Contexto: migração Asaas → Stripe concluída. Os campos guardavam dados Stripe com nome errado.
Ou seja: por um intervalo, o asaas_payment_id estava guardando PaymentIntent do Stripe. A coluna mentia sobre o próprio conteúdo. Eu integrei primeiro e renomeei depois, e durante esse intervalo qualquer pessoa lendo aquele schema — inclusive eu, em três meses — concluiria a coisa errada.
O renomear foi feito em migration dedicada, separada de qualquer lógica de negócio, justamente para poder revisar e reverter isolado. Isso foi acerto. O erro foi a ordem: o ALTER TABLE devia ter vindo antes da primeira linha de código que escrevia dado novo ali, não depois. Renomear coluna é barato e reversível; conviver com um nome que mente, não.
Decisão 3: o que apagar e o que guardar
A limpeza de 6 de março separou as colunas do Asaas em dois grupos, e o critério não foi técnico — foi se o usuário vê ou não.
Removidas, porque eram ids internos de um sistema que eu não uso mais:
ALTER TABLE payments
DROP COLUMN IF EXISTS asaas_split_id,
DROP COLUMN IF EXISTS asaas_transfer_id,
DROP COLUMN IF EXISTS asaas_boleto_barcode,
DROP COLUMN IF EXISTS asaas_pix_qrcode;Mantidas, porque são o que alguém pode querer abrir de novo:
asaas_boleto_url— o link do boletoasaas_invoice_url— o comprovanteasaas_pix_payload— o copia-e-cola do PIX
O asaas_split_id não serve para nada: é a chave de um repasse numa API que eu não chamo mais. Mas o asaas_invoice_url é o comprovante de alguém. Se essa pessoa pedir a nota de uma compra de fevereiro, eu preciso ter o link.
O critério que ficou: id interno de sistema morto pode ir; artefato que o usuário vê, fica. É a diferença entre limpar schema e apagar o passado de alguém.
E ficou uma terceira categoria, que eu documentei em vez de resolver:
-- Para remoção futura (após confirmar que nenhum provider usa Asaas):
-- provider_payment_settings.asaas_account_id
-- provider_payment_settings.asaas_wallet_id
-- asaas_webhook_events (tabela completa)Ainda estão lá. Escrever a condição de remoção no comentário foi melhor que apagar no escuro, mas "para remoção futura" sem data é só dívida com nome bonito.
Decisão 4: webhook do zero, não adaptado
Eu não tentei adaptar o handler do Asaas para o formato do Stripe. Reconstruí.
A tentação era grande — o handler antigo funcionava, e "só" o formato do payload mudava. Mas modelo de eventos não é formato de payload. O Asaas e o Stripe discordam sobre quais eventos existem, quando são emitidos, quantas vezes, e o que é a chave de identidade de cada um. Adaptar significaria carregar os pressupostos do modelo antigo para dentro do novo, onde eles estariam errados de um jeito difícil de ver.
A reconstrução é o que permitiu colocar a idempotência desde o primeiro commit do handler novo, em vez de descobri-la por incidente. O artigo anterior destrincha as camadas que saíram disso — incluindo a que eu descobri, escrevendo, que não estava ligada.
A parte que quebrou de propósito
Aqui o título cobra o preço.
O fluxo do aluno não quebrou: quem tinha crédito continuou com crédito, quem estava comprando continuou comprando. Essa parte eu protegi, e é o que o artigo 1 prometeu.
O fluxo do provider quebrou inteiro, por necessidade. KYC não se transfere entre gateways. A aprovação de conta no Asaas e o onboarding do Stripe Connect Express têm estados, requisitos e documentos diferentes, e não existe "mapear de um para o outro" — a Stripe não aceita a verificação de outra empresa, e está certa em não aceitar. Cada provider teve que passar pelo onboarding novo.
Isso é o custo de saída que eu descrevi no artigo 2 como trade-off do Connect: as contas são da Stripe, e trocar de gateway significa refazer o onboarding de todos. Eu escrevi aquele parágrafo tendo acabado de pagar essa conta.
O que dá para fazer é só o que eu fiz: avisar antes, explicar por quê, e não deixar ninguém descobrir que precisa refazer cadastro na hora que ia receber.
A lição
Migração de gateway não é um problema de integração de API. É um problema de modelagem de estado ao longo do tempo: como representar "isso aconteceu no sistema antigo" e "isso acontece no novo" sem fingir que são a mesma coisa.
As três decisões que importaram foram todas sobre o que não unificar — histórico não reescrito, colunas de display preservadas, handler novo em vez de adaptado. A vontade de ter um schema só, limpo, sem rastro do fornecedor antigo, é estética. O histórico financeiro de outra pessoa não é lugar para exercer estética.
E a que eu erraria diferente: renomear antes de escrever. Uma coluna com nome errado guardando dado certo é uma armadilha que você mesmo arma, e que só dispara quando você já esqueceu que armou.