npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

tributos-br

v1.1.0

Published

Motor de cálculo tributário brasileiro com precisão decimal arbitrária

Readme


Por que tributos-br?

  1. Audit trail em cada cálculo — quando a SEFAZ rejeita, você sabe exatamente onde o arredondamento causou a divergência. Cada função retorna um array de steps com fórmula e valor de cada etapa.

  2. DIFAL com 3 modos — base única, base dupla (LC 190/2022) e base reduzida (CST 20 com benefício fiscal). A maioria dos ERPs menores calcula DIFAL apenas com base única. tributos-br seleciona o modo via parâmetro.

  3. FECOP na MVA Ajustada — ALQ Intra Efetiva = ALQ Interna + FECOP. Ignorar FECOP gera diferença de ~4pp na MVA. Estados: RJ, MG, CE, PE, BA, GO, MT, PI.

  4. ST unificada — uma única calcSt() cobre todos os 5 cenários de Substituição Tributária via parâmetros opcionais. Sem nomenclatura ST-01 a ST-05.


O problema

JavaScript usa IEEE 754 (ponto flutuante) para números. Isso causa erros silenciosos em cálculos fiscais:

1.064 * 39680
// Esperado: 42219.52
// IEEE 754: 42219.520000000004

A SEFAZ rejeita NF-e com erros de validação 629/630 quando vProd != vUnCom x qCom. Esse drift de centavos é causado por arredondamento IEEE 754.

O tributos-br resolve isso com aritmética em strings de precisão arbitrária. Toda operação é feita sobre dígitos decimais puros, sem conversão para number em nenhum ponto intermediário.


Instalação

npm install tributos-br
yarn add tributos-br
pnpm add tributos-br

Uso rápido

import { calcSt } from 'tributos-br'

const resultado = calcSt({
  baseIcms: '1000',
  aliquotaIcms: '0.12',
  mva: '0.40',
  aliquotaSt: '0.18',
  valorIpi: '100',
})

resultado.icmsSt.toFixed(2) // '157.20'
resultado.baseSt.toFixed(2) // '1540.00'

// Audit trail — cada etapa do cálculo
resultado.audit
// [
//   { step: "Base ICMS",            formula: "1000",                        value: "1000.00" },
//   { step: "ICMS Próprio",         formula: "1000.00 x 0.1200",            value: "120.00"  },
//   { step: "Base ST (MVA + IPI)",  formula: "(1000.00 + 100.00) x 1.4000", value: "1540.00" },
//   { step: "ICMS-ST",             formula: "1540.00 x 0.1800 - 120.00",   value: "157.20"  },
// ]

Todas as funções recebem e retornam valores como Decimal, nunca number. Use .toFixed(2) para formatar e .toNumber() apenas para interoperabilidade com APIs externas.


Funções disponíveis

| Função | Descrição | | ------------------- | --------------------------------------------- | | calcIcms() | ICMS próprio (por dentro / por fora) | | calcIpi() | IPI sobre produto | | calcMvaAjustada() | MVA ajustada interestadual (com FECOP) | | calcSt() | ICMS-ST unificada (5 cenários via parâmetros) | | calcDifal() | DIFAL base única + dupla + reduzida (CST 20) | | calcCbs() | CBS (reforma tributária, LC 214/2025) | | calcIbs() | IBS (reforma tributária, LC 214/2025) |

Todas as funções são puras, recebem parâmetros (nunca hardcode de alíquotas), retornam objeto com breakdown dos cálculos + audit trail, e usam Decimal internamente com arredondamento HALF_UP (padrão SEFAZ).


Exemplos

ICMS próprio

import { calcIcms } from 'tributos-br'

// ICMS por fora (padrão, operações interestaduais)
const icms = calcIcms({
  valorProduto: '1000',
  aliquota: '0.18', // 18%
})
// icms.base    → 1000
// icms.imposto → 180

// ICMS por dentro (operações internas, imposto embutido no preço)
const icmsDentro = calcIcms({
  valorProduto: '1000',
  aliquota: '0.18',
  incluirImpostoNaBase: true,
})
// icmsDentro.base    → ~1219.51 (1000 / (1 - 0.18))
// icmsDentro.imposto → ~219.51

IPI

import { calcIpi } from 'tributos-br'

const ipi = calcIpi({
  valorProduto: '1000',
  aliquota: '0.10', // 10%
})
// ipi.base    → 1000
// ipi.imposto → 100

MVA ajustada

Corrige a MVA original para operações interestaduais, neutralizando o diferencial de alíquota (EC 87/2015).

import { calcMvaAjustada } from 'tributos-br'

const mva = calcMvaAjustada({
  mvaOriginal: '0.40', // 40%
  aliquotaInterestadual: '0.12', // 12%
  aliquotaInterna: '0.18', // 18%
})
// mva.mvaOriginal → 0.40
// mva.mvaAjustada → ~0.5024 (50,24%)

// Com FECOP (2%)
const mvaFecop = calcMvaAjustada({
  mvaOriginal: '0.40',
  aliquotaInterestadual: '0.12',
  aliquotaInterna: '0.18',
  fecop: '0.02',
})
// mvaFecop.mvaAjustada → 0.54 (54%)

ICMS-ST (Substituição Tributária)

Uma única função que cobre todos os cenários via parâmetros opcionais:

import { calcSt } from 'tributos-br'

// Cenário básico: operação interna com MVA original
const st = calcSt({
  baseIcms: '1000',
  aliquotaIcms: '0.18', // 18%
  mva: '0.40', // 40%
  aliquotaSt: '0.18', // 18%
})
// st.baseIcms    → 1000
// st.icmsProprio → 180
// st.baseSt      → 1400 (1000 x 1.40)
// st.icmsSt      → 72   (1400 x 0.18 - 180)

// Cenário interestadual com IPI compondo base ST
const stInter = calcSt({
  baseIcms: '1000',
  aliquotaIcms: '0.12',
  mva: '0.5024',
  aliquotaSt: '0.18',
  valorIpi: '100',
})
// stInter.baseSt → 1652.64 ((1000 + 100) x 1.5024)
// stInter.icmsSt → 177.47

// Cenário com pauta fiscal (SEFAZ define valor mínimo)
const stPauta = calcSt({
  baseIcms: '500',
  aliquotaIcms: '0.12',
  mva: '0.40',
  aliquotaSt: '0.18',
  pautaFiscal: '15', // R$ 15 por unidade
  quantidade: '100', // 100 unidades
})
// Se pauta (15 x 100 = 1500) > base MVA (500 x 1.40 = 700), usa pauta
// stPauta.baseSt → 1500

// Cenário com redução de base
const stReduzida = calcSt({
  baseIcms: '1000',
  aliquotaIcms: '0.18',
  mva: '0.40',
  aliquotaSt: '0.18',
  reducaoBase: '0.10', // reduz 10% da base ICMS
  reducaoBaseSt: '0.10', // reduz 10% da base ST
})
// stReduzida.baseIcms → 900 (1000 x 0.90)
// stReduzida.baseSt   → 1400 (base ST antes da redução)
// icmsSt calculado sobre base ST efetiva (1400 x 0.90 = 1260)

// Cenário com FECOP
const stFecop = calcSt({
  baseIcms: '1000',
  aliquotaIcms: '0.12',
  mva: '0.40',
  aliquotaSt: '0.18',
  fecop: '0.02', // +2% sobre alíquota ST
})
// Alíquota ST efetiva = 0.18 + 0.02 = 0.20

DIFAL (Diferencial de Alíquota)

import { calcDifal } from 'tributos-br'

// Base única (não-contribuinte, consumidor final)
const difalSimples = calcDifal({
  valorOperacao: '1000',
  aliquotaInterestadual: '0.12',
  aliquotaInternaDestino: '0.18',
  destinatarioContribuinte: false,
})
// difalSimples.icmsOrigem  → 120
// difalSimples.icmsDestino → 180
// difalSimples.difal       → 60
// difalSimples.baseDifal   → 1000

// Base dupla (contribuinte, LC 190/2022)
const difalDupla = calcDifal({
  valorOperacao: '1000',
  aliquotaInterestadual: '0.12',
  aliquotaInternaDestino: '0.18',
  destinatarioContribuinte: true,
})
// difalDupla.baseDifal   → ~1073.17 ((1000 - 120) / (1 - 0.18))
// difalDupla.icmsDestino → ~193.17
// difalDupla.difal       → ~73.17

// Base reduzida (CST 20, benefício fiscal com pRedBC)
// Caller pré-calcula a base reduzida: 243.90 × (1 - 0.95) = 12.20
const difalReduzida = calcDifal({
  valorOperacao: '12.20',
  aliquotaInterestadual: '0.12',
  aliquotaInternaDestino: '0.18',
  destinatarioContribuinte: false,
  baseReduzida: true,
})
// difalReduzida.baseDifal   → 14.88 (12.20 / (1 - 0.18), ICMS por dentro)
// difalReduzida.icmsOrigem  → 1.46
// difalReduzida.icmsDestino → 2.68
// difalReduzida.difal       → 1.22

// Com FECOP
const difalFecop = calcDifal({
  valorOperacao: '1000',
  aliquotaInterestadual: '0.12',
  aliquotaInternaDestino: '0.18',
  destinatarioContribuinte: false,
  fecop: '0.02',
})
// difalFecop.icmsDestino → 200 (1000 x (0.18 + 0.02))
// difalFecop.difal       → 80

CBS e IBS (Reforma Tributária)

import { calcCbs, calcIbs } from 'tributos-br'

// CBS — substitui PIS/COFINS (LC 214/2025)
const cbs = calcCbs({
  base: '1000',
  aliquota: '0.009', // 0,9% (alíquota teste 2026)
})
// cbs.imposto → 9

// IBS — substitui ICMS/ISS (LC 214/2025)
const ibs = calcIbs({
  base: '1000',
  aliquota: '0.178', // 17,8% (soma estadual + municipal)
})
// ibs.imposto → 178

As alíquotas não são hardcoded porque variam por setor e ano-calendário durante a transição.


Precisão Decimal

A lib inclui a classe Decimal para aritmética de precisão arbitrária. Pode ser usada independentemente:

import { Decimal, RoundingMode } from 'tributos-br/precision'

// Criação
const a = Decimal.from('1.064')
const b = Decimal.from('39680')

// Operações (encadeamento imutável)
const resultado = a.mul(b).round(2)
console.log(resultado.toFixed(2)) // '42219.52' (exato!)

// Arredondamento monetário (2 casas, HALF_UP)
Decimal.from('1.235').toMoney().toFixed(2) // '1.24'

// Arredondamento de alíquota (4 casas, HALF_UP)
Decimal.from('0.12345').toRate().toFixed(4) // '0.1235'

// Comparações
Decimal.from('1.23').gt('1.22') // true
Decimal.from('1.23').eq('1.23') // true

// Min/Max
Decimal.max('100', '200', '150').toFixed(0) // '200'
Decimal.min('100', '200', '150').toFixed(0) // '100'

Modos de arredondamento

| Modo | Descrição | | ----------- | ------------------------------------- | | HALF_UP | Metade para cima (padrão SEFAZ) | | HALF_EVEN | Metade para o par (Banker's rounding) | | HALF_DOWN | Metade para baixo | | UP | Para longe do zero | | DOWN | Para o zero (trunca) | | CEILING | Para +Infinity | | FLOOR | Para -Infinity |


Entry points

| Import | Conteúdo | | ----------------------- | ------------------------------------------------------ | | tributos-br | Calculadoras (ICMS, IPI, DIFAL, ST, CBS/IBS) + Decimal | | tributos-br/precision | Apenas Decimal + RoundingMode |

// Se você só precisa de Decimal (sem calculadoras)
import { Decimal } from 'tributos-br/precision'

// Se precisa das calculadoras (Decimal também incluso)
import { calcIcms, Decimal } from 'tributos-br'

Compatibilidade

  • Node.js >= 20
  • ESM e CommonJS
  • TypeScript strict

Motivação

A motivação veio de experiência real com cálculo de ICMS e IPI em código de produção. Arredondamentos IEEE 754 causavam rejeições de NF-e por centavos de diferença — erro 629, valor total não bate com preço unitário vezes quantidade.

Em vez de corrigir caso a caso com toFixed, a solução foi trocar a base: fazer aritmética em strings de dígitos, sem nunca passar por number nos cálculos intermediários. O resultado é uma engine de cálculo tributário que produz os mesmos valores que a SEFAZ espera, sem surpresas.

Os testes são derivados da legislação vigente e de exemplos oficiais. Valores esperados nunca vêm da implementação.


Licença

MIT