LGMdevs
Voltar ao blog
Equipe LGMdevs

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.

TypeScriptBackendArquitetura

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.js no TypeScript e numeric no 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 arredondadoRegra
Math.round (JavaScript)-2metade vai para +∞
ROUND (Excel)-3metade se afasta do zero
round(numeric) (Postgres)-3metade se afasta do zero
round(double precision) (Postgres)depende da plataformaem 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:

  1. Pegar planilhas reais, já usadas em propostas, como gabarito.
  2. Montar o mesmo orçamento pelo sistema e comparar cada total lado a lado.
  3. 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.

Fontes