PHPackages                             cboxdk/laravel-tax - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. cboxdk/laravel-tax

ActiveLibrary

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.

v0.13.0(1w ago)2572↓66.8%1MITPHPPHP ^8.4CI passing

Since Jul 15Pushed 1w agoCompare

[ Source](https://github.com/cboxdk/laravel-tax)[ Packagist](https://packagist.org/packages/cboxdk/laravel-tax)[ RSS](/packages/cboxdk-laravel-tax/feed)WikiDiscussions main Synced 1w ago

READMEChangelog (10)Dependencies (27)Versions (33)Used By (1)

Cbox Tax
========

[](#cbox-tax)

**`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`](https://github.com/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
--------------------------------------------

[](#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
---------------------------------------

[](#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 entityBuyer (FR business, validated)ResultGerman entitycross-border intra-EU B2B**reverse charge** — no VAT chargedFrench entitydomestic supply**French VAT** is charged`SellerRegistrations` (establishment + registrations) is the seller side of the calculation the billing engine supplies per invoice.

What's covered
--------------

[](#whats-covered)

RegimeStatus**EU VAT**`eu-vat` — Art. 44/45/58 place-of-supply (general B2C services source at the supplier; goods and electronic services at the customer), intra-EU B2B reverse charge, Art. 59c €10k micro-business relief scoped to the supplies it covers; 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 and intrastate-sourcing gates, with rates, 25-category taxability, nexus thresholds and sourcing rules from the **us-tax-data dataset** (all 51 jurisdictions, on by default)✅ address-exact for 30 states**Canada GST/HST**`ca-gst` — province-level combined rate, cross-border B2B self-assessment✅See [`docs/coverage`](docs/coverage/_index.md) 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. A category the dataset leaves undetermined, or one whose rule is conditional on the line amount (the MA/NY/RI clothing thresholds), **refuses** rather than defaulting to taxable — over-collecting from a consumer is a failure too. State rates, per-state taxability (25 categories) and economic-nexus thresholds are supplied by the **us-tax-data dataset**, enabled by default. **Intrastate sourcing is applied**, not just supplied: nine states tax an in-state sale at the seller's location, so give the supply a `SupplyRoute(shipFrom: …)` and a Texas in-state sale is charged the seller's rate. Interstate stays destination-sourced everywhere, and a supply with no route behaves exactly as before. **Address-exact** rates are live for **30 states**. Twenty-six need `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. Florida, Pennsylvania, Hawaii and Virginia need no opt-in and no boundary file at all, because the county is the only authority that can tax there and a geocoder returns it for free. The rest fall back to the state rate ([details](docs/coverage/us-tax-dataset.md#rooftop-zip4-into-the-boundary-index)). **Remote-seller elections close two of those states on request.** Alabama's SSUT (flat 8%) and Texas' Single Local Use Tax Rate (6.25% + 1.75% for 2026) are statutory schemes a remote seller elects into; give the state registration the `remote-election` scheme and the engine prices under them — opt-in, dated, and refusing rather than guessing when the published figure lapses. **Marketplace sales are not the seller's to collect.** Every US state with a sales tax now makes a qualifying marketplace the liable party — Missouri closed the set on 2023-01-01 — so pass `marketplaceFacilitated: true` and the engine returns `MarketplaceFacilitated`: nothing charged, because the marketplace already charged it. It is kept apart from `Exempt` and `NotRegistered` on purpose. All three are a zero and they mean opposite things on a return, and most states still expect the sale reported in gross receipts and then deducted. The rule is checked **on the supply's date**, so a backdated Missouri sale from 2022 is still the seller's.

**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`](docs/coverage/_index.md).

**EU** place of supply follows the Directive rather than a single rule: goods (Art. 33(a)) and electronically-supplied services (Art. 58) are taxed at the customer, while a general B2C service is taxed **where the supplier is established** (Art. 45) — so a German consultancy invoicing a French consumer owes German VAT. On top of that sits the **Art. 59c €10,000 micro-business threshold**, scoped to the supplies it actually covers (goods and TBE, not services generally): a below-threshold, non-opted seller charges origin VAT; opted-in or over-threshold charges destination. 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**.

Documents, not just single supplies
-----------------------------------

[](#documents-not-just-single-supplies)

A real invoice is multi-line. `TaxOrder` carries the context every line shares plus `SupplyLine[]`, and `OrderTaxCalculator::assessOrder()` returns each line's verdict tied to the id you sent:

```
$assessment = app(OrderTaxCalculator::class)->assessOrder(new TaxOrder(
    place: $geo->find(new CountryCode('DK')),
    customer: CustomerType::Consumer,
    seller: new SellerRegistrations(new CountryCode('DK')),
    pricing: Pricing::Exclusive,
    lines: [
        new SupplyLine('subscription', Money::of('100.00', 'DKK'), TaxCategory::DigitalService),
        new SupplyLine('usage',        Money::of('37.50',  'DKK'), TaxCategory::DigitalService),
        new SupplyLine('onboarding',   Money::of('2500.00','DKK'), TaxCategory::ServicesProfessional),
    ],
));

$assessment->tax();              // summed from the rounded lines, never recomputed
$assessment->forLine('usage');   // that line's own assessment
$assessment->taxByAuthority();   // per-jurisdiction totals for remittance, or null
```

The order plane adds **no** tax logic — each line becomes a single-supply query and runs the identical path, so a document cannot reach an outcome a single supply could not. A line may override the document's pricing (VAT-inclusive subscription beside exclusive usage) or carry its own exemption.

Rate breakdown
--------------

[](#rate-breakdown)

Where a rate is **stacked** from several authorities — a US state share plus the county, city and special-district records a rooftop lookup matched — the assessment carries a `TaxBreakdown` splitting the tax across them, so a seller can remit per jurisdiction. The shares are **allocated from the tax actually charged**, never recomputed per authority, so they sum to it exactly and a return reconciles with the invoices behind it. A `null` breakdown means the split is **unknown**, not that one authority takes everything. See [`docs/core-concepts/rate-breakdown.md`](docs/core-concepts/rate-breakdown.md).

Buyer exemptions
----------------

[](#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`](docs/core-concepts/exemptions.md).

Design
------

[](#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
------------

[](#requirements)

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

Development
-----------

[](#development)

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

License
-------

[](#license)

MIT.

###  Health Score

48

—

FairBetter than 94% of packages

Maintenance98

Actively maintained with recent releases

Popularity21

Limited adoption so far

Community10

Small or concentrated contributor base

Maturity52

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~1 days

Total

30

Last Release

13d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/b9761a79e61f2d5b9d650510dfb3555da18daf38f027aa84012c937e397e39a7?d=identicon)[cboxdk](/maintainers/cboxdk)

---

Top Contributors

[![sylvesterdamgaard](https://avatars.githubusercontent.com/u/2431914?v=4)](https://github.com/sylvesterdamgaard "sylvesterdamgaard (80 commits)")

---

Tags

cboxeu-vatgstlaravelreverse-chargesales-taxtaxvatlaravelvattaxsales taxeu vatgstcboxreverse-charge

###  Code Quality

TestsPest

Static AnalysisPHPStan

Code StyleLaravel Pint

### Embed Badge

![Health badge](/badges/cboxdk-laravel-tax/health.svg)

```
[![Health](https://phpackages.com/badges/cboxdk-laravel-tax/health.svg)](https://phpackages.com/packages/cboxdk-laravel-tax)
```

###  Alternatives

[psalm/plugin-laravel

Psalm plugin for Laravel

3365.5M359](/packages/psalm-plugin-laravel)[laravel/mcp

Rapidly build MCP servers for your Laravel applications.

80732.6M270](/packages/laravel-mcp)[laravel/socialite

Laravel wrapper around OAuth 1 &amp; OAuth 2 libraries.

5.7k118.2M1.0k](/packages/laravel-socialite)[laravel/scout

Laravel Scout provides a driver based solution to searching your Eloquent models.

1.7k59.5M714](/packages/laravel-scout)[illuminate/auth

The Illuminate Auth package.

10528.8M1.4k](/packages/illuminate-auth)[illuminate/routing

The Illuminate Routing package.

1419.6M3.8k](/packages/illuminate-routing)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
