https://github.com/ejosterberg/opensalestax-drupal-commerce
Drupal Commerce 3.x tax type plugin for destination-based US sales tax via the self-hosted OpenSalesTax engine. Calculation only; the merchant remits.
https://github.com/ejosterberg/opensalestax-drupal-commerce
drupal drupal-commerce opensalestax php sales-tax tax-calculation
Last synced: 22 days ago
JSON representation
Drupal Commerce 3.x tax type plugin for destination-based US sales tax via the self-hosted OpenSalesTax engine. Calculation only; the merchant remits.
- Host: GitHub
- URL: https://github.com/ejosterberg/opensalestax-drupal-commerce
- Owner: ejosterberg
- License: apache-2.0
- Created: 2026-05-14T02:15:07.000Z (about 1 month ago)
- Default Branch: main
- Last Pushed: 2026-05-14T02:40:23.000Z (about 1 month ago)
- Last Synced: 2026-05-14T04:38:04.162Z (about 1 month ago)
- Topics: drupal, drupal-commerce, opensalestax, php, sales-tax, tax-calculation
- Language: PHP
- Size: 54.7 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- Contributing: CONTRIBUTING.md
- License: LICENSE
- Security: SECURITY.md
Awesome Lists containing this project
README
# OpenSalesTax for Drupal Commerce
> **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](https://github.com/ejosterberg/opensalestax). 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/calculate` on 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's
`cache.default` bin 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](https://github.com/ejosterberg/opensalestax).
## 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
```bash
composer require ejosterberg/opensalestax-drupal-commerce
drush en opensalestax_commerce -y
drush cache:rebuild
```
The Composer install transparently pulls in the
[`ejosterberg/opensalestax`](https://packagist.org/packages/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
1. At checkout, Drupal Commerce's tax pipeline iterates over enabled
tax types and calls `applies($order)` on each.
2. Our plugin's `applies()` short-circuits to `FALSE` on non-US,
non-USD, missing ZIP, or missing shipping profile.
3. When `applies()` returns `TRUE`, Drupal Commerce calls `apply($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](https://packagist.org/packages/ejosterberg/opensalestax).
4. For each tax line returned, we write a per-jurisdiction
`Drupal\commerce_order\Adjustment` of type `tax` with the
jurisdiction's name as label and `opensalestax:` as
source ID.
5. 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
```bash
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`](CONTRIBUTING.md) for branch model, DCO sign-off,
and the quality gate.
## Security
See [`SECURITY.md`](SECURITY.md) for responsible-disclosure guidance and
[`docs/SECURITY-REVIEW.md`](docs/SECURITY-REVIEW.md) for the threat
model with mitigation status.
## Related projects
- [OpenSalesTax engine](https://github.com/ejosterberg/opensalestax)
- [OpenSalesTax PHP SDK](https://github.com/ejosterberg/opensalestax-php)
- [opensalestax-magento](https://github.com/ejosterberg/opensalestax-magento)
- [opensalestax-woocommerce](https://github.com/ejosterberg/opensalestax-woocommerce)
- [opensalestax-vendure](https://github.com/ejosterberg/opensalestax-vendure)
- [opensalestax-medusa](https://github.com/ejosterberg/opensalestax-medusa)
- [opensalestax-saleor](https://github.com/ejosterberg/opensalestax-saleor)
## License
Dual-licensed under your choice of [Apache-2.0](LICENSE-APACHE.txt) OR
[GPL-2.0-or-later](LICENSE-GPL.txt). See [`LICENSE`](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`](CONTRIBUTING.md).