cboxdk/laravel-tax

Cbox Tax — a self-hostable consumption-tax engine for Laravel: it owns the calculation logic (place-of-supply, reverse-charge, rate application, inclusive/exclusive) per jurisdiction and sources only rate data behind a pluggable contract.

Maintainers

Package info

github.com/cboxdk/laravel-tax

pkg:composer/cboxdk/laravel-tax

Transparency log

Statistics

Installs: 529

Dependents: 1

Suggesters: 0

Stars: 1

Open Issues: 0

v0.8.2 2026-08-05 20:02 UTC

README

cboxdk/laravel-tax — a self-hostable consumption-tax engine for Laravel. It owns the calculation logic — place-of-supply, reverse-charge, rate application, inclusive/exclusive — and sources only the rate data behind a pluggable contract. No forced third-party calculation SaaS.

Built on cboxdk/laravel-geo: every supply is assessed against a jurisdiction resolved from canonical ISO data, so tax is a function of (seller registrations, buyer jurisdiction, product type) — never a fuzzy country-name match.

The boundary: own the logic, source the data

use Cbox\Tax\Contracts\TaxCalculator;
use Cbox\Tax\ValueObjects\TaxQuery;
use Cbox\Tax\ValueObjects\SellerRegistrations;
use Cbox\Tax\Enums\{CustomerType, Pricing};
use Cbox\Geo\ValueObjects\CountryCode;
use Brick\Money\Money;

$assessment = app(TaxCalculator::class)->assess(new TaxQuery(
    amount: Money::of('100.00', 'EUR'),
    pricing: Pricing::Exclusive,
    place: $geo->find(new CountryCode('FR')),   // buyer jurisdiction (from laravel-geo)
    customer: CustomerType::Business,
    seller: new SellerRegistrations(new CountryCode('DE')),
    customerTaxIdValidated: true,               // VIES-validated
));

$assessment->treatment;   // TaxTreatment::ReverseCharge — intra-EU B2B, buyer self-accounts
$assessment->tax;         // Money 0.00 EUR
$assessment->reason;      // human-readable explanation for the audit trail

The engine decides whether and how to tax; the TaxRateSource contract supplies the rate number — the EU Commission's TEDB called live (no API key), the compiled US dataset, or a commercial adapter. A missing rate is refused, never assumed 0%.

Multi-entity / seller-of-record routing

Tax depends on which selling entity issues the invoice. The same buyer is taxed differently by a German entity vs a French one:

Selling entity Buyer (FR business, validated) Result
German entity cross-border intra-EU B2B reverse charge — no VAT charged
French entity domestic supply French VAT is charged

SellerRegistrations (establishment + registrations) is the seller side of the calculation the billing engine supplies per invoice.

What's covered

Regime Status
EU VAT eu-vat — Art. 44/45/58 place-of-supply, intra-EU B2B reverse charge, Art. 59c €10k micro-business origin/destination sourcing; rates live from the Commission's TEDB
National VAT/GST UK, CH, NO, AU, NZ, MX, SG, TW, UAE, SA, BH, OM, TR, CL, ID, VN, PH, JP, KR, TH, UA
India in-gst — dual GST (IGST vs CGST+SGST), OIDAR destination, B2B reverse charge
Malaysia my-sst — SST service tax; charges B2B+B2C, no reverse charge
US sales tax us-sales-tax — nexus/taxability/sourcing logic, with rates, 25-category taxability, nexus thresholds and sourcing from the us-tax-data dataset (all 51 jurisdictions, on by default) ✅ rooftop for 26 states
Canada GST/HST ca-gst — province-level combined rate, cross-border B2B self-assessment

See docs/coverage for the full per-country table with sources and confidence — and an honest list of jurisdictions we omit until their rate data is verified (a broad national-VAT batch pending primary-source confirmation, Pakistan's other provinces, and Brazil). We omit rather than ship a rate we cannot stand behind.

The US regime gates on three things before applying a rate — the state must be resolved (via the AddressGeocoder), the seller must have nexus in it, and the product must be taxable there — otherwise it returns NotRegistered or Exempt, never a wrong charge. State rates, per-state taxability (25 categories), economic-nexus thresholds and intrastate sourcing are supplied by the us-tax-data dataset, enabled by default. Rooftop resolution is live for 26 states with us_tax_data.rooftop enabled: the 24 Streamlined states resolve by ZIP+4 through the published boundary index — Kansas City comes out as 6.5% state + 1.0% county + 1.625% city — while California and New Mexico resolve by point against their own polygon services. The rest fall back to the state rate (details). Canada resolves at province level (no local tax). Rate data plugs in via TaxRateSource: set TAX_TEDB_LIVE=true to resolve EU rates from the Commission's own TEDB service (no key, no registration, cached per country), or bind a commercial adapter — see docs/coverage.

EU additionally applies the Art. 59c €10,000 micro-business threshold: a below-threshold, non-opted seller charges origin VAT; opted-in or over-threshold charges destination (signals supplied on the seller). Rate sources resolve by taxability category, so reduced/zero bands apply when a bound source supplies them (none are fabricated by default).

Unmodelled jurisdictions and missing rates are refused, not guessed.

Buyer exemptions

A query may carry a native buyer exemption (a resale / nonprofit / government certificate) on TaxQuery::$exemption. Applied deny-by-default over the regime's verdict, a valid exemption that covers the taxed jurisdiction rewrites a would-be Standard line to Exempt (net kept, tax 0, gross = net) with the certificate reference recorded on the assessment; reverse-charge, not-registered and zero-rated outcomes are left untouched, and an exemption for a different jurisdiction or an expired one does not exempt. The engine computes the assessment; certificate capture and verification are the consumer's concern. See docs/core-concepts/exemptions.md.

Design

  • Contracts-first. TaxCalculator, TaxRegime, TaxRateSource, RegimeRegistry, AddressGeocoder, VatIdValidator, ReturnAggregator — bind and override any of them. Rate sources compose (static · remote · caching · chain).
  • Deny-by-default. No regime for a jurisdiction, or no rate, → an exception, never a silent zero.
  • Money is exact. Amounts are brick/money; rate maths rounds half-up once.

Requirements

PHP ^8.4 with ext-dom; Laravel ^13. See composer.json.

Development

composer install
composer qa    # pint --test, phpstan (level max), pest, license-check, audit

License

MIT.