tax
Tax, VAT and pricing-rules capability pack: US state sales tax and international VAT/GST/consumption tax from a built-in reference rate table (salesTax, vat, listRates), add tax net-to-gross...
tax — tax / VAT rules, discounts & subscription proration
A pure Leumas2 domain adapter pack (zero npm deps, Node built-ins only). It is the reusable rules engine for consumption tax, discounts, and subscription proration. All money math runs through cent-accurate rounding to avoid IEEE-754 float drift, and results are plain JSON.
What it does
| Tool | Purpose |
|---|---|
salesTax | US state sales tax (or explicit rate) net → gross, from the built-in table |
vat | International VAT/GST; inclusive:true backs tax out of a gross amount |
addTax | net → gross given a rate |
removeTax | gross → net (back out the tax) |
compoundTax | stack multiple rates — sequential (tax-on-tax) or parallel (state+county+city) |
discount | apply ONE discount: percent, fixed, or coupon (with optional min floor) |
tieredPrice | graduated bracket pricing (graduated) or single-tier flat (flat) |
volumeDiscount | bulk per-unit discount by quantity breaks |
proration | prorate a mid-cycle subscription change by remaining-period fraction (charge/credit) |
marginPrice | cost-plus pricing — margin (% of price) or markup (% of cost) |
listRates | dump the reference rate table (us / intl / coupons / all) or one region |
Rate table
data/rates.json is a static reference table: US 50 states + DC (combined state-level base sales tax), 30+ countries (standard national VAT/GST/consumption tax), and a small coupon catalog. Rates are decimal fractions (0.0725 = 7.25%). These are indicative reference values for estimation, not tax advice — for filing, use an authoritative source. Pass an explicit rate to bypass the table.
Rates accept either form: a decimal fraction (0.2) or a percent (20) — any value > 1 is read as a percent.
Usage
import tax from '@leumas/adapter-tax'; // or the registry: registry.run('tax', 'salesTax', {...})
tax.adapters.salesTax({ amount: 100, region: 'CA' });
// → { net: 100, rate: 0.0725, tax: 7.25, gross: 107.25, region: 'US-CA', regionName: 'California', taxType: 'sales' }
tax.adapters.vat({ amount: 120, region: 'GB', inclusive: true });
// → { inclusive: true, net: 100, tax: 20, gross: 120, rate: 0.2, ... }
tax.adapters.compoundTax({ amount: 100, rates: [0.05, { rate: 0.07, label: 'PST' }], mode: 'sequential' });
tax.adapters.discount({ amount: 80, coupon: 'SAVE20' });
// → { original: 80, kind: 'coupon:percent', discount: 16, final: 64, ... }
tax.adapters.tieredPrice({ quantity: 250, tiers: [{ upTo: 100, price: 1 }, { upTo: 200, price: 0.8 }, { upTo: null, price: 0.5 }] });
tax.adapters.proration({ amount: 30, periodStart: '2026-01-01', periodEnd: '2026-02-01', changeDate: '2026-01-16', direction: 'charge' });
tax.adapters.marginPrice({ cost: 70, marginPercent: 30, mode: 'margin' }); // → price 100
Every tool takes ONE args object (maps 1:1 onto an HTTP POST body) and throws TypeError/RangeError on bad input; unknown region/coupon lookups throw a status: 404 error.
DRY boundary (respect it)
- finance owns money-over-time math: compound interest, loan amortization, TVM / NPV / IRR.
- invoice owns billing documents: line items → a rendered invoice, invoice numbers, late fees.
- tax (this pack) owns tax rules, discount rules, and subscription proration — the
numbers you compute before they land in an invoice line or a finance model.
Where they touch (an invoice line carries a taxRate/discount), invoice does its own inline math; this pack is the standalone engine you call to derive tax and discount figures — and a natural companion to PassNode for prorating plan/seat changes mid-cycle.