NewWe open-sourced 50+ Laravel packages
Custom AI apps, agents and automation — Roundly ConsultingRoundly
All packages

TaxRate computes net, tax and gross with one exact rational and one rounding per call — no float divisor, no int conversion:

use RoundlyConsulting\Money\Tax\TaxRate;

$vat   = TaxRate::fromBasisPoints(2000, 'VAT');   // 20 %
$net   = Money::ofMajor('10', 'EUR');
$gross = Money::ofMajor('12', 'EUR');

$vat->taxOnNet($net);         // 2.00 EUR
$vat->grossFromNet($net);     // 12.00 EUR
$vat->netFromGross($gross);   // 10.00 EUR
$vat->taxInGross($gross);     // 2.00 EUR

$line = TaxRate::of('19')->breakdownFromGross(Money::ofMajor('10', 'EUR'));
$line->net;     // 8.40 EUR
$line->tax;     // 1.60 EUR
$line->gross;   // 10.00 EUR — net + tax === gross, always

TaxRate::of('8.875', 'Sales tax');   // precise to a thousandth of a percent
TaxRate::zero();
  • TaxRate::of($percent, ?$label), TaxRate::fromBasisPoints($bp, ?$label), TaxRate::zero().
  • taxOnNet() and grossFromNet() — for prices without tax.
  • netFromGross() and taxInGross() — for prices that include tax.
  • breakdownFromNet() rounds the tax and derives the gross; breakdownFromGross() rounds the net and derives the tax.

In both directions net + tax === gross exactly: one side rounds, the other is derived by subtraction. The package property-tests this on 10 000 seeded amounts across all eight rounding modes.

Invoice summaries

use RoundlyConsulting\Money\Tax\TaxSummary;

$vat     = TaxRate::of(20, 'VAT');
$reduced = TaxRate::of(10, 'Reduced');

$summary = TaxSummary::of(
    $vat->breakdownFromNet(Money::ofMajor('10', 'EUR')),
    $vat->breakdownFromNet(Money::ofMajor('25', 'EUR')),
    $reduced->breakdownFromNet(Money::ofMajor('8', 'EUR')),
);

$summary->perRate();   // list<TaxBreakdown>, one per rate, sorted by rate:
                       // 10 %: 8.00 + 0.80 = 8.80 · 20 %: 35.00 + 7.00 = 42.00
$summary->net();       // 43.00 EUR
$summary->tax();       // 7.80 EUR
$summary->gross();     // 50.80 EUR

TaxSummary re-aggregates lines without re-rounding. Whether tax is rounded per line or per total is your policy — which is why both primitives exist. Mixed currencies throw CurrencyMismatch.

Show your open-source love

This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.

More ways to support, including crypto

By donating, you agree to our donation terms.

Want this built into your product?

We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.