ejosterberg / opensalestax-drupal-commerce
Drupal Commerce 3.x tax type plugin that calculates destination-based US sales tax via a self-hosted OpenSalesTax engine.
Package info
github.com/ejosterberg/opensalestax-drupal-commerce
Type:drupal-module
pkg:composer/ejosterberg/opensalestax-drupal-commerce
Requires
- php: >=8.2
- ejosterberg/opensalestax: ^0.1
- psr/log: ^1.0 || ^2.0 || ^3.0
Requires (Dev)
- phpstan/phpstan: ^1.11
- phpunit/phpunit: ^10.5
This package is auto-updated.
Last update: 2026-05-19 18:29:32 UTC
README
v0.1.1. Live-validated against Drupal 11 + Drupal Commerce 3.3.5 on PHP 8.4 ($100 / MN ZIP 55401 → 6 per-jurisdiction adjustments totalling $9.03). Passes 56 unit tests on PHP 8.2–8.4; PHPStan level max clean; composer audit clean. CI green on
main.
A free, self-hostable Drupal Commerce 3.x tax type plugin that swaps manual tax-rate tables for destination-based US sales tax via the OpenSalesTax engine. No per-transaction fees, no SaaS lock-in — merchants run both Drupal Commerce and OpenSalesTax on their own infrastructure.
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.
What this module does
- Registers OpenSalesTax as a Drupal Commerce Tax Type plugin
(
@CommerceTaxType(id = "opensalestax")). Drupal Commerce auto-discovers it once the module is enabled. - When a US/USD order with a 5-digit shipping ZIP reaches the tax
pipeline, the plugin calls
POST /v1/calculateon your engine and writes one tax adjustment per jurisdiction onto the order (so the cart and order screens render "Minnesota State Sales Tax", "Hennepin County Tax", etc. — not a single opaque tax line). - Caches responses per
(zip5, line-signature)in Drupal'scache.defaultbin for 24 hours by default. - Falls back silently (no tax line, no fatal) on non-US, non-USD, missing ZIP, or any engine error.
What this module does NOT do
- File or remit tax — calculation only. The merchant remits.
- Validate addresses.
- Handle non-USD currencies or non-US destinations (passes those through, no tax line written).
- Handle tax-exempt customers, customer groups, or per-store-entity configuration. (v0.2+.)
- Tax shipping lines. (v0.2+.)
- Ship with the engine bundled — point it at your own OpenSalesTax engine.
Compatibility matrix
| Drupal core | Drupal Commerce | PHP | Status |
|---|---|---|---|
| 10.3+ | 3.x | 8.1+ | tested |
| 11.0+ | 3.x | 8.1+ | should work |
The module hard-pins calculation-only behavior — no schema changes, no service overrides. It coexists with Drupal Commerce's built-in flat-rate tax types and applies first when its applies() gate matches.
Install
composer require ejosterberg/opensalestax-drupal-commerce drush en opensalestax_commerce -y drush cache:rebuild
The Composer install transparently pulls in the
ejosterberg/opensalestax
PHP SDK.
Configure
Visit Commerce → Configuration → OpenSalesTax
(/admin/commerce/config/opensalestax).
| Field | Default | Purpose |
|---|---|---|
| Engine API URL | (empty) | Base URL of your OpenSalesTax engine, e.g. https://ost.example.com. Empty = module inert. |
| API Key (optional) | (empty) | X-API-Key header value if your engine requires authentication. Stored as a config string; blank-field-on-save preserves the existing key. |
| Restrict to public IPs (SSRF defense) | ON | Reject any engine URL whose host resolves to a private, loopback, link-local, CGNAT, or multicast IP. Turn OFF only when the engine is on the same private network as Drupal (e.g. http://10.x.x.x:8080). |
| Cache TTL (seconds) | 86400 (24h) | How long to cache engine responses per (zip5, line-signature). Minimum 3600. |
| Engine HTTP timeout (seconds) | 10 | Maximum wait for the engine before falling back. |
| Fail hard on engine error | OFF | When ON, an unreachable engine blocks checkout. When OFF (default), the failure is logged and checkout proceeds with no tax line. |
Then add OpenSalesTax (Destination-Based US Sales Tax) as the Tax Type on each store via Commerce → Configuration → Taxes.
How it works
- At checkout, Drupal Commerce's tax pipeline iterates over enabled
tax types and calls
applies($order)on each. - Our plugin's
applies()short-circuits toFALSEon non-US, non-USD, missing ZIP, or missing shipping profile. - When
applies()returnsTRUE, Drupal Commerce callsapply($order). We normalize the order into(country, currency, zip5, line_items[]), look up the cache, and on miss call the engine via the PHP SDK. - For each tax line returned, we write a per-jurisdiction
Drupal\commerce_order\Adjustmentof typetaxwith the jurisdiction's name as label andopensalestax:<jurisdiction>as source ID. - Drupal Commerce's totals pipeline picks the adjustments up and renders them.
If anything goes wrong (engine down, timeout, bad payload), and
Fail hard on engine error is OFF (default), the failure is logged
via Drupal's opensalestax logger channel and no adjustments are
written — checkout proceeds without tax. The merchant then resolves
the engine outage at their own pace without customer-visible breakage.
Logging
All engine interactions log structured metadata
(zip5, http_status, error message) via Drupal's opensalestax
logger channel. Customer addresses and full payloads are never
logged. The API key is read from config in memory only at request
time and never written to logs.
Development
composer install composer test # PHPUnit unit suite (56 tests) composer stan # PHPStan level max composer audit # composer audit (HIGH+ blocking)
CI runs the same three checks plus a DCO sign-off check on PRs.
See CONTRIBUTING.md for branch model, DCO sign-off,
and the quality gate.
Security
See SECURITY.md for responsible-disclosure guidance and
docs/SECURITY-REVIEW.md for the threat
model with mitigation status.
Related projects
- OpenSalesTax engine
- OpenSalesTax PHP SDK
- opensalestax-magento
- opensalestax-woocommerce
- opensalestax-vendure
- opensalestax-medusa
- opensalestax-saleor
License
Dual-licensed under your choice of Apache-2.0 OR
GPL-2.0-or-later. See LICENSE for the
dual-declaration. Drupal contrib code lives under GPL-2.0-or-later;
this dual license keeps the module eligible for future Drupal.org
listing while preserving Apache-2.0 compatibility for downstream
redistribution.
DCO sign-off
Every commit signed off with -s. CI rejects unsigned commits. See
CONTRIBUTING.md.