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: 191

Dependents: 1

Suggesters: 0

Stars: 0

Open Issues: 0

v0.1.3 2026-07-17 18:25 UTC

This package is auto-updated.

Last update: 2026-07-17 18:27:03 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 (an EU TEDB feed, the SST files, 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
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; per-state taxability, rooftop rates and nexus thresholds are data you must bind ⚠️ logic only
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 is logic only, not production-ready: it 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. But the per-state SaaS taxability map, rooftop local rates (the shipped GeocodioGeocoder resolves state-level only), and economic-nexus thresholds are DATA that is not shipped and must be bound before US customers can be invoiced correctly. Canada resolves at province level (no local tax). Rate data (TEDB via TedbRateSource, SST, commercial) plugs in via TaxRateSource — 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.

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; Laravel ^13. See composer.json.

Development

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

License

MIT.