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

invkit

v0.1.0

Published

Invoice calculator with multiple tax types, item and total discounts, and extras. Full-precision math with rounding only at output.

Readme

InvKit

CI license: MIT

Deterministic invoice math for TypeScript—multiple tax models, stacked discounts with explicit order, and extras—computed in full precision and rounded only when you read totals. Suitable for backends that need stable numbers and for interactive UIs that recompute as the cart changes.

InvKit is intentionally small, side-effect free, and dependency-free at runtime. The public API is a thin wrapper around two calculator modes:

Disclaimer: InvKit provides deterministic arithmetic only. It is not legal, accounting, or tax advice.

| API | When to use | |-----|-------------| | InvKit.once(inputs) | Immutable / one-shot: you pass the full snapshot (items, taxes, discounts, extras) and read totals. Ideal for server-side pricing, quotes, and PDFs—no mutable instance to keep in sync. | | InvKit.dynamic(inputs?) | Mutable / interactive: same getters, plus methods to add, update, or remove lines and rules. Each mutating call invalidates cached figures; reads trigger a full recalculation. Built for carts, editors, and live previews. |

Both paths apply the same order of operations and rounding rules, so server and client stay aligned.


Why InvKit?

Invoice logic looks simple until production exposes the edge cases:

  • Tax ordering – Applying taxes in the wrong sequence changes every line. InvKit applies line taxes in a fixed order: inclusive → normal → compound → withdraw → fixed, matching common regulatory patterns.
  • Compound taxes – “Compound” here means taxes chain on the running line total in that order (normal before compound, etc.), so stacked percentages behave predictably.
  • Withholding taxes – Modeled as withdraw (rate or fixed off the line), so net amounts after VAT-style taxes and withholding stay consistent.
  • Stacked discounts – Item-level and invoice-level discounts stack with optional priority (lower first); otherwise array order applies—no ambiguous “sometimes percentage first” behavior.
  • Rounding – Intermediate steps use full floating-point precision; getTotal, getLineTotal, and discount display amounts round to two decimal places in a stable way, avoiding cumulative drift from rounding at every step.

Install

bun add invkit
# or
npm install invkit

For local development after cloning this repository, dependencies are resolved with:

bun install

The published artifact is built with Bun’s bundler (bun build): ESM, --target bun, --production (minified), external source maps, and bunx tsc --emitDeclarationOnly for .d.ts beside dist/index.js (declaration layout mirrors src/). CI uses Bun only (bun test, bun run build).

bun test
bun run build
bun run typecheck

Peer (optional): TypeScript ^5.0.0 for project type-checking. You can import public types directly:

import type { Inputs, Item, Taxes } from 'invkit'

Quick start

One-shot (immutable)

Use when the full invoice state is known up front (API handler, batch job, immutable domain object).

import InvKit from 'invkit'

const invoice = InvKit.once({
  items: [
    { name: 'Widget', price: 100, quantity: 2, taxes: ['VAT'] },
    { name: 'Service', price: 50, quantity: 1, discount: [{ name: 'promo', type: 'fix', value: 5 }] },
  ],
  taxes: [{ name: 'VAT', type: 'normal', value: 20 }],
  discount: [{ name: 'loyalty', type: 'rate', value: 10 }],
  extra: [{ name: 'shipping', type: 'additional', value: 8 }],
})

invoice.getTotal()                        // 278.4
invoice.getSubtotalBeforeGlobalDiscount() // 302.4
invoice.getLineTotal(0)                   // 240 (100×2 + 20% VAT)
invoice.getLineTotal(1)                   // 54 (50−5 + 20% VAT, then rounded)
invoice.getDiscount()                    // [{ name: 'loyalty', type: 'rate', value: 10, amount: 30.24 }]
invoice.listExtra()                      // [{ name: 'shipping', type: 'additional', value: 8, amount: 8 }]
invoice.findItem('Widget')               // { name: 'Widget', price: 100, quantity: 2, taxes: ['VAT'] }

Dynamic (mutable)

Use when lines and rules change over time (shopping cart, admin UI). Call InvKit.dynamic() with no args for an empty invoice, or pass partial Inputs to seed state; totals update when you call getters after changes.

import InvKit from 'invkit'

const invoice = InvKit.dynamic()

invoice.addItem({ name: 'A', price: 10, quantity: 2 })
invoice.addItem({ name: 'B', price: 25, quantity: 1, taxes: ['VAT'] })
invoice.setTaxes([{ name: 'VAT', type: 'normal', value: 10 }])
invoice.setDiscount([{ name: 'sale', type: 'rate', value: 5 }])
invoice.addExtra({ name: 'ship', type: 'additional', value: 5 })

invoice.getTotal()  // recalculated after each mutation
invoice.modifyItem(0, { quantity: 3 })
invoice.removeItem(1)
invoice.getTotal()  // updated again

Order of operations

  1. Per line: amount ?? price × quantityitem discounts (by priority, then array order) → item taxes (invariant order: inclusive → normal → compound → withdraw → fixed).
  2. Subtotal = sum of raw line totals.
  3. Global discounts (same priority / order rules as item discounts).
  4. Extras (additional / subtraction).

Rounding runs only on the values you read (getTotal, getLineTotal, getDiscount()[].amount, etc.), not after each intermediate operation. The final total is one rounding pass on the fully computed subtotal, which keeps results stable compared with rounding every step.


Validation & edge cases

InvKit focuses on predictable arithmetic, not business-rule validation. It does not throw on “invalid” business data; callers should enforce catalog and pricing policies upstream.

| Scenario | Behavior | |----------|----------| | Missing tax references | If an item lists taxes: ['VAT'] but no definition with name: 'VAT' exists in the invoice’s taxes array, that name is ignored for that line—no error. | | Negative values | Negative price, quantity, or amount are not rejected. Math proceeds as written; results may not match real-world pricing rules. Validate sign and ranges in your domain layer. | | Fixed discount overflow | For type: 'fix', the applied amount is capped at the current line or subtotal so a fixed discount cannot push a positive base below zero by itself. Rate discounts use a percentage of the current value. | | Decimal quantities | quantity may be fractional (e.g. weight or hours). Line base is price × quantity using the same precision rules as other steps. | | Non-finite inputs | roundMoney is used for getTotal, getLineTotal, getSubtotalBeforeGlobalDiscount, and getDiscount() amounts. If the value passed in is not finite, those reads return 0. Raw fields (e.g. listExtra() amount when sourced from value) are not coerced the same way—keep inputs numeric. |


Input types

Inputs

| Field | Type | Required | Description | |------------|--------------|----------|-------------| | items | Item[] | yes | Line items. | | taxes | Taxes[] | no | Global tax definitions (referenced by name from items). | | discount | Discount[] | no | Invoice-level discounts (after line totals). | | extra | Extra[] | no | Charges or credits after discounts. |

Item

| Field | Type | Required | Description | |------------|--------------|----------|-------------| | name | string | yes | Label. | | price | number | yes | Unit price (ignored when amount is set). | | quantity | number | no | Defaults to 1. | | amount | number | no | When set, replaces price × quantity for the line base. | | taxes | string[] | no | Names of taxes to apply; each name must match a Taxes.name entry to take effect. | | discount | Discount[] | no | Discounts applied only to this line. |

Taxes (tax definition)

| Field | Type | Description | |---------|------------|-------------| | name | string | Identifier referenced from items (taxes: ['VAT']). | | type | TaxTypes | See below. | | value | number | Percentage for rate-based types, or fixed amount for fixed / rate-off-fixed cases. |

Tax types

  • inclusive – Treats the line as already tax-inclusive; does not change the line amount.
  • normal – Adds tax (rate: line × (1 + value/100); non-rate uses add fixed).
  • compound – Same formula as normal; participates in the ordered tax chain.
  • withdraw – Withholding-style reduction (rate: line × (1 − value/100), or subtract fixed).
  • fixed – Adds a fixed amount to the line.

Taxes on a line always run in this order: inclusive → normal → compound → withdraw → fixed.

Discount

| Field | Type | Description | |------------|------|-------------| | name | string | Label. | | type | 'rate' \| 'fix' | rate: percentage of the current amount. fix: fixed deduction, capped at the current amount. | | value | number | Percent (rate) or amount (fix). | | priority | number | Optional. Lower values apply first; if omitted, array order applies. |

Same semantics for line and invoice-level discounts.

Extra

| Field | Type | Description | |---------|------|-------------| | name | string | Label. | | type | 'additional' \| 'subtraction' | Adds or subtracts from the total after global discounts. | | value | number | Amount applied. |


API

Static calculator (InvKit.once(inputs))

| Method | Returns | Description | |--------|---------|-------------| | getTotal() | number | Final total (rounded). | | getSubtotalBeforeGlobalDiscount() | number | Sum of line totals before global discounts (rounded). | | listItem() | Item[] | All items. | | findItem(name) | Item \| undefined | First item with that name. | | getLineTotal(index) | number \| undefined | Line total (rounded); undefined if index out of range. | | getDiscount() | (Discount & { amount?: number })[] | Global discounts with computed amount. | | listExtra() | (Extra & { amount?: number })[] | Extras with amount. | | findExtra(name) | (Extra & { amount?: number }) \| undefined | First extra with that name. |

Dynamic calculator (InvKit.dynamic(inputs?))

Same getters as above, plus mutators. After any of the methods below, the next read of totals or lines runs a full recalculation.

| Method | Description | |--------|-------------| | addItem(item) | Append an item. | | modifyItem(index, updates) | Merge Partial<Item> at index. | | removeItem(index) | Remove the item at index. | | setDiscount(discount) | Replace the global discount list. | | setTaxes(taxes) | Replace tax definitions. | | addExtra(extra) | Append an extra. | | modifyExtra(index, updates) | Merge Partial<Extra> at index. | | removeExtra(index) | Remove the extra at index. |


Examples

E-commerce: mixed items, VAT, coupon, shipping

InvKit.once({
  items: [
    { name: 'T-Shirt', price: 29.99, quantity: 2, taxes: ['VAT'] },
    { name: 'Gift card', price: 50, quantity: 1 },
  ],
  taxes: [{ name: 'VAT', type: 'normal', value: 20 }],
  discount: [{ name: 'SUMMER20', type: 'rate', value: 20 }],
  extra: [{ name: 'Shipping', type: 'additional', value: 4.99 }],
})

B2B: VAT + withholding tax

InvKit.once({
  items: [{ name: 'Consulting', price: 1000, quantity: 1, taxes: ['VAT', 'WHT'] }],
  taxes: [
    { name: 'VAT', type: 'normal', value: 20 },
    { name: 'WHT', type: 'withdraw', value: 5 },
  ],
})
// Line: 1000 → +20% VAT = 1200 → −5% WHT = 1140

Discount priority: fix then rate

InvKit.once({
  items: [{ name: 'Product', price: 100, quantity: 1 }],
  discount: [
    { name: 'coupon', type: 'fix', value: 10, priority: 1 },
    { name: 'member', type: 'rate', value: 10, priority: 2 },
  ],
})
// Subtotal 100 → −10 = 90 → −10% = 81

Testing

bun test

Coverage includes basic totals, order of operations, all tax types, item- and invoice-level discounts (with priority), extras, precision (e.g. 1.005 → 1.01), edge cases (empty invoice, amount override, no taxes, out-of-range indices), and dynamic add / modify / remove / set flows.

License

MIT