Dinheiro no código: como reproduzir um orçamento ao centavo
Ponto flutuante, modo de arredondamento e o momento em que se arredonda decidem se o seu sistema bate com a planilha do cliente. O que aprendemos validando um sistema de orçamento de obra contra planilhas reais.
Um engenheiro orçamentista só troca a planilha por um sistema quando o sistema chega no mesmo número que ele confere na mão. Construindo o Click Orçamentos, validamos o cálculo contra orçamentos reais, linha a linha, e o custo direto bateu ao centavo. Chegar lá exigiu três decisões que valem para qualquer software que lida com dinheiro: o tipo numérico, o modo de arredondamento e o momento em que se arredonda.
1. number não serve para dinheiro
O number do JavaScript é um ponto flutuante binário (IEEE 754). Ele representa muito bem
grandezas físicas, e mal valores decimais exatos:
0.1 + 0.2 === 0.3 // false: o resultado é 0.30000000000000004
(1.005).toFixed(2) // "1.00": 1.005 é guardado como 1.00499999...Num orçamento com milhares de multiplicações, esses resíduos se acumulam e aparecem no centavo. As saídas são duas:
- Inteiros em centavos, quando toda a conta cabe em duas casas. Simples e rápido, mas quebra quando há preço unitário com mais casas, índices como BDI ou encargos sociais.
- Um tipo decimal de verdade, como
decimal.jsno TypeScript enumericno Postgres. Foi a nossa escolha: os cálculos guardam 8 casas e só arredondam onde a regra de negócio manda.
Um detalhe que pega muita gente: JSON não tem tipo decimal. Um número que sai da API
como 1234.5678 é lido como number do outro lado, e a precisão se perde no transporte.
Trafegue valores monetários como string ("1234.5678") e converta para decimal na borda.
2. Arredondar não é uma operação só
"Arredondar para duas casas" esconde uma escolha: o que fazer com o 5 exato? Cada ferramenta responde de um jeito:
| Ferramenta | -2,5 arredondado | Regra |
|---|---|---|
Math.round (JavaScript) | -2 | metade vai para +∞ |
ROUND (Excel) | -3 | metade se afasta do zero |
round(numeric) (Postgres) | -3 | metade se afasta do zero |
round(double precision) (Postgres) | depende da plataforma | em geral, metade para o par |
Se a referência do cliente é uma planilha, o sistema precisa usar a mesma regra da
planilha. Com decimal.js, isso é explícito em cada chamada:
import Decimal from "decimal.js"
// Mesma regra do ROUND do Excel: a metade se afasta do zero
const arredondar = (valor: Decimal) => valor.toDecimalPlaces(2, Decimal.ROUND_HALF_UP)3. Arredondar em cada linha ou só no total
Esta foi a diferença que mais apareceu na validação. Planilhas de orçamento costumam usar
ROUND(preço × quantidade; 2) em cada linha e somar os valores já arredondados. Um
sistema que guarda todas as casas e arredonda só no total chega a outro número, e os
dois estão "certos".
Com três linhas ilustrativas, a diferença já aparece:
const linhas = ["10.005", "20.005", "30.005"].map((v) => new Decimal(v))
const porLinha = linhas.reduce((total, v) => total.plus(arredondar(v)), new Decimal(0))
const noTotal = arredondar(linhas.reduce((total, v) => total.plus(v), new Decimal(0)))
porLinha.toFixed(2) // "60.03"
noTotal.toFixed(2) // "60.02"Um centavo em três linhas vira dezenas de reais num orçamento com centenas de serviços. Arredondar só no final é mais preciso. Arredondar por linha é o que o engenheiro confere com a calculadora, e o que vai impresso na proposta. Isso é regra de negócio, não detalhe de implementação: decida com quem usa o sistema e escreva a decisão no código, num único lugar.
4. Valide contra o mundo real, não contra você mesmo
Teste unitário com números inventados prova que o código faz o que você pensou, não que ele faz o que o cliente faz. O que funcionou para nós:
- Pegar planilhas reais, já usadas em propostas, como gabarito.
- Montar o mesmo orçamento pelo sistema e comparar cada total lado a lado.
- Classificar toda diferença: bug do sistema, diferença de modelo (como o momento do arredondamento) ou erro da própria planilha.
A terceira categoria existe. Na validação do Click Orçamentos, a diferença que restou veio de um imposto desatualizado na planilha, copiado de uma versão anterior do arquivo. O sistema estava certo. Esse tipo de achado só aparece quando o teste usa dados reais.
Rode a mesma bateria várias vezes. Cálculo financeiro precisa ser determinístico: o mesmo orçamento, calculado de novo, tem que dar exatamente o mesmo resultado.
Quando isto não se aplica
Se o seu sistema só soma valores que já chegam prontos em duas casas, como o total de um carrinho com preços fechados, inteiros em centavos resolvem e uma biblioteca decimal é peso desnecessário. A complexidade deste post se paga quando há multiplicação por quantidades, índices e percentuais encadeados, que é o caso de orçamentos, impostos e folha de pagamento.
Tem um sistema cujos números não batem com a planilha do cliente? Fale com a gente.