Desconto em valor absoluto precisa ser aplicado na base
O que aconteceu
O desconto de membro nasceu percentual (discount_percentage) e era aplicado escalando o item de parcela já calculado, pelo fator 1 - pct/100. Isso funcionava porque os juros de parcelamento são lineares sobre a base, então escalar o total final era matematicamente idêntico a descontar a base:
(base × fator) × (1 + juros) == (base × (1 + juros)) × fator
Ao trocar o desconto para valor em reais, essa equivalência caiu — e a implementação existente passou a produzir um preço diferente do prometido.
Causa raiz
Dois problemas distintos apareceram juntos.
Percentual com 2 casas não alcança valores exatos
Não é bug de arredondamento, é representabilidade. Com decimal(5,2) de percentual sobre uma base de R$ 897, os descontos alcançáveis andam de ~R$ 0,09 em R$ 0,09:
897 → 697 exige 22,2965…%
22,30% resulta em 697,09
Quando o negócio precisa cravar um preço final (“de 897 por 697”), o desconto tem que ser expresso no mesmo domínio do preço — reais — e não em percentual.
Subtração não comuta com juros
A identidade acima vale para fator multiplicativo, não para subtração:
(base − X) × (1 + juros) ≠ (base × (1 + juros)) − X
Concretamente, com base 897, juros de 2% ao mês lineares e desconto de R$ 200 em 12x:
| Onde abater | Total | Parcela | Desconto efetivo |
|---|---|---|---|
| Na base (adotado) | 864,28 | 72,02 | R$ 248,00 |
| No total final | 912,28 | 76,02 | R$ 200,00 |
Correção
Adotamos abater na base: o desconto reduz o preço do produto e os juros incidem sobre o valor já descontado. Isso preserva o comportamento que o percentual tinha — ele também reduzia os juros — e faz o à vista bater exatamente no preço prometido.
- O desconto deixou de ser uma transformação do item pronto e passou a ser a troca da base antes do cálculo de juros.
Checkout#discount_factoreCheckout#apply_discount(item)foram removidos em favor deCheckout#discounted_total, consumido via o parâmetrooveride_totalque#installment_totaljá aceitava. CheckoutPaymentType#build_installment_itemcalcula a própria base e não passa porCheckout#installment_total, então precisou do mesmo tratamento (kwargapply_discount:) — senão os checkouts que usamcheckout_payment_typescobrariam preço cheio.- O frontend não pode mais espelhar a conta escalando os totais: precisaria duplicar a fórmula de juros. Passou a consumir os preços já descontados que a API devolve em
validate_discount, e oapplyCheckoutDiscount.jsfoi deletado.
Como evitar
- Quando o negócio promete um preço final exato, expresse o desconto no mesmo domínio do preço (reais), não em percentual.
- Antes de reaproveitar uma transformação de preço, verifique se a operação comuta com o cálculo de juros. Fator multiplicativo comuta; subtração não.
- A fórmula de juros vive só no servidor. Se o frontend precisa exibir preço descontado, a API devolve o preço pronto — não a regra para o cliente recalcular.
Comportamento atual: ../reference/checkout/member_discount.md.