Skip to content
Insights

A coluna chamada amount_cents vai cobrar 100x a mais do Japão

5 min de leituraPagamentos

Quase todo schema de pagamento começa igual. Alguém aprende a não guardar dinheiro em float — corretamente — e escreve:

amount_cents integer not null

Isso está certo para dólar, euro, libra e para quase tudo que você vai faturar. Está errado na primeira vez que você cobra um cliente japonês, e está errado por um fator de cem.

Moedas sem casa decimal

O iene japonês não tem subunidade. ¥1.000 é mil ienes, não dez ienes e um troco. Won coreano, dong vietnamita, peso chileno, guarani paraguaio e vários outros se comportam igual. A Stripe e a maioria das APIs de pagamento tratam isso certo — recebem o valor na menor unidade da moeda, e para o JPY a menor unidade É o iene.

Então uma coluna chamada amount_cents guardando 100000 significa mil dólares, e cem mil ienes. Se seu código multiplica o número humano por 100 na entrada, você acabou de faturar ¥100.000 por um trabalho de ¥1.000.

Moedas de três casas também existem

Dinar do Bahrein, dinar kuwaitiano, dinar jordaniano e rial omanense usam três casas decimais. A menor unidade é um milésimo. Um schema que assume duas casas cobra dez vezes menos nessas moedas.

A correção é nomenclatura e uma função

Guarde o valor na menor unidade da própria moeda, e nomeie a coluna de um jeito que ninguém possa ler errado. Depois centralize a conversão para que exista exatamente um lugar que conhece o expoente:

const ZERO_DECIMAL = new Set(["JPY", "KRW", "VND", "CLP", "PYG", "ISK"]);
const THREE_DECIMAL = new Set(["BHD", "KWD", "JOD", "OMR", "TND"]);

function exponent(currency: string): 0 | 2 | 3 {
  const c = currency.toUpperCase();
  if (ZERO_DECIMAL.has(c)) return 0;
  if (THREE_DECIMAL.has(c)) return 3;
  return 2;
}

export function toMinorUnits(amount: number, currency: string): number {
  return Math.round(amount * 10 ** exponent(currency));
}

Duas regras sustentam isso: a coluna crua nunca é lida direto fora desse módulo, e nenhum código de exibição divide por 100. Se algum template contém um 100 literal, ali está o bug esperando acontecer.

E mais: recuse o que você não consegue representar

Se alguém digitar ¥1.000,50, esse valor não existe. Arredondar em silêncio é uma decisão que sua contabilidade vai herdar. Valide na entrada e recuse — um erro no cadastro sai mais barato que uma divergência na conciliação.

Nada disso é difícil. Só é invisível até a primeira fatura numa moeda que ninguém testou, e a essa altura o dinheiro já se moveu.

Construindo algo onde essas decisões importam?

Iniciar projeto