Pular para o conteúdo
PHP

Parcelamento do Mercado Pago sem chamar a API

Paloma Macetko
Um unicornio de crina colorida ao lado de um abaco de cristais, com moedas douradas subindo em escada e um cartao de credito magico

Olá meus Unicórnios! 🦄✨

Aquele pedido inocente: "coloca na página do ingresso um ou até 12x de R$ 10,17, igual às lojas grandes". Meia hora, no máximo. 😅 Pois é.

O problema é que o valor da parcela não é o preço dividido por 12. Tem juros no meio, e quem manda nos juros é o Mercado Pago, não eu. O caminho óbvio é perguntar a ele — existe uma API que responde exatamente isso. Só que essa página é a mais visitada do site, e chamar uma API externa a cada visita, só para desenhar uma frase, é caro e frágil: se o Mercado Pago demora, minha página demora junto.

Então a conta passou a ser feita offline, aqui mesmo no PHP. E foi aí que apareceu o bug que dá nome a este artigo — um bug meu, de sete centavos, que ficou meses passando despercebido. 😳

Exemplos_MercadoPagoParcelamento no GitHubO código completo deste artigo: uma página PHP onde você digita o valor e vê a tabela de parcelas — com a coluna que prova que a soma fecha.github.com

🧮 O coeficiente: a conta que o Mercado Pago já fez por você

A primeira coisa que eu tentei foi bonita e errada: aplicar a fórmula de juros compostos (a Tabela Price, aquela do financiamento) com a taxa que aparece no painel. Os números quase batiam com o checkout. Quase.

Não bate porque o Mercado Pago não te entrega uma taxa mensal para você calcular em cima — ele te entrega o resultado pronto. Na resposta da API de parcelamento vem um campo chamado installment_rate, que é o acréscimo percentual sobre o total daquela opção. Ou seja: a conta difícil já foi feita. O que sobra para mim é uma multiplicação.

Transformando esse percentual em multiplicador:

coeficiente = 1 + (installment_rate / 100)

Com isso, cada quantidade de parcelas vira um número só. Multiplique o preço por ele e você tem o total com juros:

function CoeficientesMercadoPago()
{
    return array(
         1 => 1.0000,
         2 => 1.0964,
         3 => 1.1123,
         4 => 1.1136,
         5 => 1.1431,
         6 => 1.1432,
         7 => 1.1672,
         8 => 1.1673,
         9 => 1.1969,
        10 => 1.2065,
        11 => 1.2066,
        12 => 1.2211,
    );
}

Repare no detalhe curioso: os valores andam de dois em dois. O 3x (1,1123) e o 4x (1,1136) são quase idênticos; o 5x e o 6x também; o 7x e o 8x também. Não é erro de digitação — é assim que as faixas de taxa do Mercado Pago funcionam mesmo. Parcelar em 4x custa praticamente o mesmo que em 3x, o que é uma informação bem útil para quem está comprando.

Para pegar os seus coeficientes (eles variam por conta e por bandeira), é uma chamada só, feita uma vez, na mão:

curl "https://api.mercadopago.com/v1/payment_methods/installments?payment_method_id=visa&amount=100&access_token=SEU_TOKEN"

💸 Centavos, sempre centavos

Antes de dividir qualquer coisa, uma regra que vale para dinheiro em geral: não faça contas em float. Em ponto flutuante, 0.1 + 0.2 não dá 0.3, e esse resíduo microscópico reaparece justamente na hora de somar doze parcelas e comparar com o total.

A solução é velha e infalível: multiplique tudo por 100 logo na entrada, trabalhe com números inteiros, e só divida por 100 na hora de mostrar na tela.

// Valor zero ou negativo nao gera opcao nenhuma -- evita divisao por
// zero e um "12x de R$ 0,00" na tela.
if ($Valor <= 0) {
    return $Opcoes;
}

$ValorCentavos = (int) round($Valor * 100);

Aquele if não é paranoia decorativa. Ingresso de cortesia custa R$ 0,00, e sem essa linha o comprador vê a página oferecendo "12x de R$ 0,00" com toda a seriedade do mundo. 🙈

🐛 Sete centavos que ninguém viu

Se você só for ler um pedaço deste artigo, leia este. 🙏

Tendo o total com juros, falta dividir pelo número de parcelas. E aqui eu fiz o que parecia a coisa mais responsável do mundo: arredondei para cima. Afinal, arredondar para baixo faria a loja receber menos, certo? Melhor pecar pelo excesso.

$TotalComJuros = round($ValorCentavos * $Coeficiente);
$ValorParcela  = ceil($TotalComJuros / $NumeroParcelas);   // 😬

Parece inofensivo. Não é. Vamos pegar R$ 100,00 em 8x, com o coeficiente 1,1673:

total com juros  =  10000 x 1,1673  =  11673 centavos  =  R$ 116,73
parcela          =  ceil(11673 / 8) =   1460 centavos  =  R$  14,60

o que a pagina anuncia:   R$ 116,73
o que o comprador soma:   8 x R$ 14,60  =  R$ 116,80
                                            ---------
                                            R$   0,07 a mais

Isso mesmo, sete centavos! 🤯 A página promete um total e exibe parcelas que somam outro. Ninguém vai à falência por sete centavos, mas é o tipo de coisa que um cliente atento manda print e pergunta — e você não tem resposta boa.

E o pior: o erro cresce com o número de parcelas, porque cada parcela carrega seu próprio arredondamento. Rodei os doze casos para R$ 100,00 e nove deles não fecham.

A correção é de duas linhas, e é a mesma que banco usa: divida inteiro nas primeiras parcelas e deixe a última absorver o resto.

function DividirEmParcelas($TotalCentavos, $NumeroParcelas)
{
    $Primeiras = intdiv($TotalCentavos, $NumeroParcelas);
    $Ultima    = $TotalCentavos - ($Primeiras * ($NumeroParcelas - 1));

    return array('primeiras' => $Primeiras, 'ultima' => $Ultima);
}

Agora o mesmo caso fecha exatamente:

7 parcelas de R$ 14,59  +  1 ultima de R$ 14,60  =  R$ 116,73  ✓

Repare que é intdiv() e não round() nas primeiras. Usar round() ali reabriria o mesmo buraco pelo outro lado: a última parcela teria que devolver a diferença e poderia ficar menor que as outras — ou até negativa, num valor bem pequeno. Com intdiv(), a última é sempre a maior, no máximo alguns centavos acima. É por isso que aquela sua fatura sempre tem a última parcela com um centavo a mais. 😄

Para não confiar só no exemplo bonito, rodei a verificação em todos os valores de R$ 0,01 até R$ 200,00, contra as doze opções — 240 mil casos:

casos=240000  erros=0

🧾 Juntando tudo

Com as duas peças no lugar, a função principal fica curta. Ela tem dois modos, porque na prática a loja pode querer bancar os juros em algumas parcelas (o famoso "3x sem juros", onde quem paga o Mercado Pago é o lojista):

foreach (CoeficientesMercadoPago() as $n => $Coeficiente) {

    $TotalCentavos = (int) round($ValorCentavos * $Coeficiente);
    $Divisao       = DividirEmParcelas($TotalCentavos, $n);

    // Parcela abaixo do minimo: para por aqui. As proximas so
    // ficariam menores ainda.
    if ($n > 1 && $Divisao['primeiras'] < $MinimoCentavos) {
        break;
    }

    $Opcoes[] = array(
        'parcelas'     => $n,
        'valorParcela' => $Divisao['primeiras'] / 100,
        'valorUltima'  => $Divisao['ultima'] / 100,
        'valorTotal'   => $TotalCentavos / 100,
        'acrescimo'    => ($TotalCentavos - $ValorCentavos) / 100,
        'semJuros'     => ($n == 1),
    );
}

Aquele break merece uma palavra, porque ele não é um continue disfarçado. Quando a parcela fica abaixo do mínimo, todas as opções seguintes serão ainda menores — não há motivo para continuar testando. E o mínimo importa de verdade: sem ele, um ingresso de R$ 10,00 aparece na tela oferecendo 12x de R$ 1,02, o que é constrangedor e nem sequer é aceito pelo cartão.

O detalhe do modo "sem juros" é que ele não usa coeficiente nenhum: é divisão pura, e o valorTotal continua igual ao preço à vista. Faz sentido — quem está pagando os juros ali é a loja, e isso não aparece para o comprador.

🖥️ A prova na tela

Aqui é onde eu deixo de acreditar em mim mesma e passo a conferir. A página de exemplo tem uma última coluna que faz uma coisa só: soma as parcelas de verdade, em centavos, e compara com o total anunciado.

// A prova: soma as parcelas de verdade, em centavos, e compara.
$SomaCentavos  = (int) round($Opcao['valorParcela'] * 100) * ($Opcao['parcelas'] - 1)
               + (int) round($Opcao['valorUltima'] * 100);
$TotalCentavos = (int) round($Opcao['valorTotal'] * 100);
$Confere       = ($SomaCentavos === $TotalCentavos);

É essa coluna que transforma o artigo em algo que você pode verificar em vez de acreditar:

Tabela de parcelamento de R$ 100,00, de 1x a 12x, com colunas de parcela, ultima parcela, acrescimo e total; a ultima coluna mostra sim em todas as linhas

Doze linhas, doze "sim". E o mais divertido: troque as duas linhas da DividirEmParcelas() pelo ceil() errado, recarregue, e a coluna vira NÃO em nove das doze linhas, com a diferença em centavos ao lado. O bug deste artigo acontece na sua frente em trinta segundos — e o README explica qual linha trocar.

🚦 Uma ressalva importante

Este cálculo é para exibir, não para cobrar. 🙂

Quem decide o valor da transação é o Mercado Pago, no momento da autorização — o número que vale é o que volta na resposta da API. A conta offline serve para a vitrine: aquela frase na página do produto, o resumo do carrinho, o e-mail de confirmação. Se a tabela de coeficientes envelhecer, o comprador vai pagar o valor certo de qualquer jeito; o que fica errado é a promessa que você fez antes.

Por isso o comentário em cima do array não é enfeite. É o único aviso que existe.

📦 O código completo

Está tudo no repositório, em dois arquivos: a lógica em parcelamento.php e a página de teste em publico/index.php. Sem Composer, sem dependência, sem chave de API — roda com php -S e pronto.

Exemplos_MercadoPagoParcelamento no GitHubClone, rode com php -S 127.0.0.1:3009 -t publico, e teste com os seus valores. O README ensina a reproduzir o bug dos centavos de propósito.github.com

Por hoje é só, meus unicórnios! 🦄✨

Que a magia do arco-íris continue brilhando em suas vidas! Até mais! 🌈🌟

PHP

Leia também