surfaai · parte 3 de 8
Destination charges na prática: split, estorno e a matemática das taxas
01 de outubro de 20265 min de leituraCesar Eduardo Sturmer
O artigo anterior foi sobre a decisão: por que deleguei a infraestrutura financeira à Stripe em vez de construir o split na mão. Este é sobre como isso funciona por dentro — incluindo uma conta que quase todo mundo erra na primeira tentativa.
Onde a cobrança acontece
O Stripe Connect tem dois padrões principais de split. A diferença entre eles é onde a cobrança nasce.
Com separate charges and transfers, você cobra na conta da plataforma e depois cria um Transfer separado para o vendedor. São duas operações, em dois momentos, que você orquestra.
Com destination charges, a cobrança já nasce sabendo para onde parte dela vai:
const paymentIntent = await stripe.paymentIntents.create({
amount: calculation.amountInCents,
currency: "brl",
payment_method_types: [billingType === "card" ? "card" : billingType],
application_fee_amount: calculation.applicationFeeAmount,
transfer_data: {
destination: stripeAccountId,
},
description,
metadata: { serviceId, providerId /* ... */ },
});Uma operação. A Stripe cobra do aluno, retém application_fee_amount para a plataforma e transfere o líquido para a conta Connect do provider assim que a cobrança é capturada.
Três consequências que importam:
- O aluno vê uma cobrança só, no nome da plataforma. Um lançamento no extrato, não dois.
- O provider recebe automaticamente, sem nenhuma rotina de repasse minha.
- Eu nunca fico com o dinheiro dele em custódia.
O detalhe que me fez escolher: estorno
Essa é a parte pouco comentada, e foi decisiva.
Quando você estorna uma cobrança feita com destination charge, a application_fee correspondente é revertida automaticamente, na proporção do estorno. A Stripe ajusta os dois lados numa operação só.
Parece um detalhe contábil, mas olhe o que ele elimina. O surfaai tem cancelamento com direito de arrependimento: dentro de sete dias, o aluno recebe estorno real, proporcional aos créditos que não usou. Com split manual, eu teria que escrever a lógica de "devolver minha taxa também", lidar com o caso de o provider já ter sacado o valor, e reconciliar tudo isso.
Com destination charges, o estorno proporcional é uma chamada:
await stripe.refunds.create({
payment_intent: payment.stripe_payment_intent_id,
amount: toCents(valorProporcional),
});E a taxa volta sozinha, na proporção certa. Zero código meu para a parte financeira do cancelamento.
As taxas não vêm da API
Para mostrar ao aluno quanto custa cada meio de pagamento no checkout, eu preciso saber a taxa antes de criar a cobrança. A tentação é consultar a Stripe. Eu não consulto.
As taxas vivem numa tabela do banco, carregadas por loadStripeFees(), com um fallback hardcoded no código:
export const STRIPE_BR_FEES: StripeFeesConfig = {
card: {
domestic: { percentageFee: 3.99, fixedFee: 0.39 },
international: { percentageFee: 5.99, fixedFee: 0.39 },
installmentExtraPercentPerMonth: 0,
},
pix: { percentageFee: 1.19, fixedFee: 0, capAmount: 999.0 },
boleto: { percentageFee: 0, fixedFee: 3.45 },
};Duas decisões empilhadas aqui.
Por que não a API: o checkout precisa ser instantâneo. Uma chamada externa só para exibir um número coloca latência e um ponto de falha num lugar onde o usuário está decidindo se compra. Taxas de gateway mudam em escala de meses, não de segundos.
Por que no banco, e não só no código: quando a Stripe reajusta uma taxa, eu quero corrigir pelo painel administrativo, não abrir um pull request e esperar deploy. O valor hardcoded é a rede de segurança para quando o banco não responde.
A conta que quase todo mundo erra
Aqui está a parte interessante. O surfaai permite configurar quem paga a taxa do gateway: a plataforma, o provider ou o aluno. Quando a taxa é repassada ao aluno, aparece um problema circular.
A intuição diz: calcule a taxa sobre o preço e some.
Com um serviço de R$ 100 e cartão a 3,99% + R$ 0,39:
taxa = 100 × 0,0399 + 0,39 = R$ 4,38
total cobrado = R$ 104,38Está errado. A Stripe não cobra a taxa sobre os R$ 100 — ela cobra sobre o valor da transação, que agora é R$ 104,38:
taxa real = 104,38 × 0,0399 + 0,39 = R$ 4,56Faltam R$ 0,18. Pouco numa venda, relevante em milhares delas, e sempre saindo do seu bolso.
O valor correto exige resolver a circularidade. Chamando o preço de base, a taxa percentual de p e a fixa de f:
total = base + taxa
taxa = total × p + fSubstituindo e isolando:
total = base + (total × p + f)
total − total × p = base + f
total = (base + f) / (1 − p)E a taxa, que é o que eu preciso somar:
taxa = (base × p + f) / (1 − p)Que é exatamente o que está no código:
export function calculateStripeFeeGrossUp(/* ... */): number {
const { percentageFee, fixedFee } = getStripeFee(
billingType, installments, isInternational, fees,
);
const pct = percentageFee / 100;
// PIX tem cap — verificar se o gross-up também atingiria o cap
if (billingType === "pix") {
const fee = round2((pct * baseAmount + fixedFee) / (1 - pct));
return fee > fees.pix.capAmount ? fees.pix.capAmount : fee;
}
// ...
}Conferindo com os números de antes:
taxa = (100 × 0,0399 + 0,39) / (1 − 0,0399)
= 4,38 / 0,9601
= R$ 4,56
total = R$ 104,56
verificação: 104,56 × 0,0399 + 0,39 = R$ 4,56 ✓Fecha. Esse padrão tem nome — gross-up — e aparece em qualquer lugar onde um valor percentual é calculado sobre um total que o inclui: imposto retido, comissão sobre comissão, taxa repassada.
Repare também no tratamento do cap do PIX. Um método com teto de taxa precisa verificar o limite depois do gross-up, não antes: a fórmula pode empurrar o valor acima do teto, e aí o teto é que vale.
O que eu levei disso
Destination charges substituiu, com duas propriedades de um objeto, o que seria um módulo inteiro de custódia e reconciliação. O estorno automático da taxa sozinho já pagaria a escolha.
Mas a lição que ficou é outra: delegar a infraestrutura não te isenta de entender a mecânica. O gross-up não é problema da Stripe, é meu. Se eu tivesse tratado o split como caixa-preta, estaria perdendo centavos em cada transação — e centavos em volume são a diferença entre margem e prejuízo.
No próximo artigo, um bug de verdade: como um aluno apertando F5 duas vezes gerou crédito dobrado, e o que isso me ensinou sobre onde o dinheiro pode ser criado.