A coluna chamada amount_cents vai cobrar 100x a mais do Japão
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 nullIsso 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