ejosterberg / opensalestax-for-woocommerce
WooCommerce plugin: replace TaxJar/Avalara with self-hosted OpenSalesTax — destination-based US sales tax at checkout.
Package info
github.com/ejosterberg/opensalestax-for-woocommerce
Type:wordpress-plugin
pkg:composer/ejosterberg/opensalestax-for-woocommerce
Requires
- php: >=8.2
- ejosterberg/opensalestax: ^0.1
Requires (Dev)
- 10up/wp_mock: ^1.0
- friendsofphp/php-cs-fixer: ^3.59
- phpstan/phpstan: ^1.11
- phpunit/phpunit: ^9.6
README
Replace TaxJar / Avalara / WooCommerce Tax with self-hosted OpenSalesTax. Free, open-source, US sales-tax calculation at WooCommerce checkout.
Status: v0.4.1. Tested against WordPress 6.6+ / WooCommerce 10.7 / OpenSalesTax engine v0.39. 109 unit tests + an end-to-end integration test on a real WP+WooCom Proxmox VM.
What this saves you
Most WooCommerce tax plugins are paid services or limited free tiers:
| Service | Pricing model |
|---|---|
| WooCommerce Tax (Jetpack) | Free up to 200 transactions/mo; paid tier above |
| TaxJar | from $19/mo + transaction fees |
| Avalara AvaTax | enterprise pricing |
| OpenSalesTax + this plugin | $0 software cost, self-hosted |
You run a small VM (or container) for the OpenSalesTax engine; this plugin calls into it from WC's checkout flow. Tax math runs locally on infrastructure you own.
Install
cd /path/to/your/wordpress/wp-content/plugins/ git clone https://github.com/ejosterberg/opensalestax-for-woocommerce.git cd opensalestax-for-woocommerce composer install --no-dev
Activate via WP Admin → Plugins.
Or via Composer:
composer require ejosterberg/opensalestax-for-woocommerce
The SDK (ejosterberg/opensalestax) and the plugin (ejosterberg/opensalestax-for-woocommerce) are both on Packagist.
For the full step-by-step walkthrough including engine setup, configuration, and verification, see docs/INSTALL.md.
Configure
- Stand up an OpenSalesTax engine (5-minute Docker quickstart)
- In WP Admin: WooCommerce → Settings → Tax → OpenSalesTax
- Enter your engine's base URL (e.g.
http://your-engine:8080) - (Optional) Enter API key if your engine has authentication enabled
- Click Test Connection — should report
status: okwith the engine version
Done. The next cart that includes a US shipping address gets destination-based sales tax.
How it works
The plugin hooks WooCommerce's woocommerce_calc_tax filter. On every line that needs tax computed:
- Read the customer's shipping ZIP from
WC()->customer - Resolve the line's tax category (WC tax class → OpenSalesTax category)
- Skip if customer is tax-exempt (
WC()->customer->is_vat_exempt()) - Look up cached tax for
(zip, category, line-amount)— return early on hit - On cache miss, call
POST /v1/calculateon your OpenSalesTax engine - Return the calculated tax amount in WC's expected format
Tax breakdown (per-state, per-county, per-city, per-district) is computed and returned by the engine. The combined total flows into WC's tax line; per-jurisdiction detail can be inspected via wp opensalestax calc <zip> <amount>.
Design choices
- Replace, not populate. This plugin replaces WC's tax calculation entirely via the filter. WC's
wp_woocommerce_tax_ratestable is ignored. Single source of truth = your OpenSalesTax engine. No DB sync drift. - Tax-exempt customers are honored via
WC()->customer->is_vat_exempt(). The exempt flag short-circuits before the engine call. - Caching uses WP transients with a default 60-minute TTL. Configurable. Bulk-flushed on settings save.
- Error handling is graceful: if the engine is unreachable, you choose between blocking (no tax line, transaction fails until config is fixed) or zero (charge $0 tax, log via WC error log). Default = block.
Compatibility
- WordPress 6.2+ (Blocks-stable)
- WooCommerce 8.2+ (security-supported)
- PHP 8.2+ (uses class-level
readonlysyntax via the OpenSalesTax SDK) - OpenSalesTax engine v0.22+ (recommended — a state-bleed bug was fixed in v0.22)
Compatible with classic checkout AND Cart/Checkout Blocks (Blocks invoke the same woocommerce_calc_tax filter via the Store API).
HPOS-compatible: this plugin doesn't post-process orders in v0.1, so the HPOS tax-reading caveat doesn't apply.
What's shipping
- ✅ Refund handling (v0.4.1) — when you issue a refund (full or partial), the refund order's admin page shows a prorated per-jurisdiction tax breakdown with negative values. Math:
parent_breakdown × (refund_total / parent_total) × -1. No engine round-trip needed. - ✅ Engine-unreachable admin notice (v0.4.1) — every WP-admin page renders a red banner when the OpenSalesTax engine is down or misconfigured. Closes the silent-failure gap where merchants could collect wrong tax for days without realizing.
- ✅ WooCommerce Subscriptions integration (v0.4.0) — renewal orders get a fresh tax recalc against current rates instead of inheriting the parent sub's stale tax line. Per-jurisdiction breakdown captured on the renewal too. No-op without WC Subscriptions installed.
- ✅ Admin-UI tax-class mapper (v0.3.3) — replaces the CLI-only configuration with a UI under WC > Settings > Tax > OpenSalesTax. Auto-discovers all WC tax classes (built-in + custom), shows a dropdown per row, includes a "Reset all to defaults" checkbox.
- ✅ Recent-calculations debug log (v0.3.2) — opt-in 50-entry ring buffer captures every cart calculation (cache-hit / engine-call / error) with timing, ZIP, category, amount, tax. Viewable via
wp opensalestax recent-calcsor a panel on the settings page. Useful when troubleshooting "why is this tax wrong?" - ✅ Status dashboard widget (v0.3.1) — WP-admin home page shows engine reachability, version, placeholder-rate state, and today's order count with breakdown captured. 60-second transient cache keeps the engine from getting hammered.
- ✅ Per-order jurisdiction breakdown (v0.3) — every order stores the engine's full state/county/city/district split as meta and renders a clean table on the WC admin order-edit page. Useful for audit reconciliation and answering "where did my tax money go?"
- ✅ WC Tax Class custom mapping (v0.2) — map
clothing,groceries, or any custom WC class to the right OST category (or mark non-taxable). Built-in defaults still apply forstandard/reduced-rate/zero-rate. Configure viawp opensalestax tax-class-list / tax-class-set / tax-class-reset. - ✅ WP-CLI commands —
test-connection,cache-flush,calc <zip> <amount>,placeholder-rate,tax-class-list,tax-class-set,tax-class-reset - ✅ SSRF mitigation (v0.1.2) — engine base URL validated against private/loopback/CGNAT ranges (opt-in for LAN deployments)
- ✅ Tax-line aggregation fix (v0.1.1) —
WC_Cart::get_tax_totals()correctly labels the line as "OpenSalesTax" via the placeholder rate row
What's coming next
- WP.org plugin directory submission (planned after launch traction)
- More commerce platforms (Stripe Tax replacement, Magento, Saleor, Medusa, ERPNext, Odoo) under the OpenSalesTax umbrella project
Out of scope
- Multi-currency carts — engine is USD-only; non-USD throws
- Stripe Connect / multi-vendor marketplace tax allocation
- Per-product custom tax-code overrides
- Tax filing / remittance — calculation only, by design (engine constitution §13)
Disclaimer
Tax calculations are provided as-is for convenience. The merchant is solely responsible for tax-collection accuracy and remittance to the appropriate jurisdictions. Verify against your state Department of Revenue before remitting.
Quality bar
- PHPStan level=max — zero suppressed errors
- PHP-CS-Fixer with PSR-12 + risky rules — zero violations
- PHPUnit unit tests against fixtures + integration tests against a real WP+WooCom instance
- GitHub Actions CI matrix on PHP 8.2 / 8.3 / 8.4
Contributing
DCO sign-off (git commit -s) required on every commit. See CONTRIBUTING.md.
License
Dual-licensed: Apache-2.0 OR GPL-2.0-or-later — recipient
picks. See LICENSE, LICENSE-APACHE.txt,
and LICENSE-GPL.txt. The GPL-2.0-or-later
option exists primarily for WordPress.org plugin directory
compatibility; most merchants embedding the plugin in their own
WooCommerce deployment will be served by the Apache-2.0 option.