Skip to content

Releases: ejosterberg/opensalestax-for-woocommerce

v0.6.2 — WP-org reviewer feedback round 1

Choose a tag to compare

@ejosterberg ejosterberg released this 17 May 17:16

WordPress.org reviewer feedback (round 1) addressed:

  1. Requires Plugins: woocommerce header added — WP 6.5+ uses this to enforce the WooCommerce dependency at activation time. Bumps Requires at least: 6.5 to match.

  2. SDK pinned to stable v0.1.1 — was effectively dev-main due to a leftover vcs repositories block in composer.json that took precedence over Packagist. The block has been removed; composer.lock now resolves to v0.1.1 (2026-05-15) from Packagist directly. The plugin ZIP attached here ships the stable SDK release.

No runtime / functional changes vs v0.6.1. This release only fixes the plugin header + dependency lockfile + (as a side benefit) the build-time bug where prior ZIPs accidentally bundled dev dependencies (PHPUnit, PHPStan, etc.) — the new ZIP runs through composer install --no-dev for ~58% size reduction.

See CHANGELOG.md for full details.

v0.6.1 — Fix Plugin URI / Author URI collision (WP-org rejection)

Choose a tag to compare

@ejosterberg ejosterberg released this 16 May 02:19

v0.6.0's bulk-update of the plugin header during the slug rename inadvertently set Author URI to the same value as Plugin URI. WP-org's submission pre-check rejected the upload (the two URIs must be different — one describes the plugin, one describes the author). Author URI reverted to the GitHub profile (https://github.com/ejosterberg).

No functional changes. 116 tests, PHPStan max, PHPCS, composer audit all green.

v0.6.0 — slug rename to opensalestax-for-woocommerce

Choose a tag to compare

@ejosterberg ejosterberg released this 16 May 01:11

Summary

Renames the plugin slug opensalestax-woocommerceopensalestax-for-woocommerce to comply with WordPress.org's trademark policy. The bare woocommerce term in plugin slugs is restricted by WP-org unless paired with one of the allowed patterns: for woocommerce, with woocommerce, using woocommerce, or and woocommerce. Plugin Check flagged this as a WARNING in v0.5.1; the WP-org reviewer treats it as a hard reject.

Changed

  • Main PHP file renamed opensalestax-woocommerce.phpopensalestax-for-woocommerce.php (via git mv — blame and history preserved).
  • Text domain renamed across every __() / _e() / esc_html__() / esc_attr__() call in src/ and the main file. Plugin-header Text Domain: updated to match.
  • Plugin headers Plugin URI: and Author URI: updated to the new GitHub repository URL.
  • Composer package name ejosterberg/opensalestax-woocommerceejosterberg/opensalestax-for-woocommerce. Old Packagist package marked abandoned with replacement: ejosterberg/opensalestax-for-woocommerce; existing composer require ejosterberg/opensalestax-woocommerce in consumers' composer.json gets a deprecation warning pointing at the new package.
  • WC logger source slug renamed (wc_get_logger()->warning(..., ['source' => 'opensalestax-for-woocommerce'])) so the new log channel matches the new plugin slug.
  • GitHub repository renamed ejosterberg/opensalestax-woocommerceejosterberg/opensalestax-for-woocommerce. GitHub auto-installs a redirect from the old URL; existing clones and links continue to work.

Unchanged

  • User-visible plugin name — still "OpenSalesTax for WooCommerce" (already user-friendly).
  • PHP namespaces — still OpenSalesTax\WooCommerce\… (not slug-derived). Class names unchanged.
  • Internal option keys (opensalestax_*) — none contained woocommerce.
  • Enqueue handles (opensalestax-admin) — already prefix-clean.
  • Behavior — no functional changes; no settings migration required.

Verified

  • Plugin Check (WP-CLI mode, run on VM 907 against the deployed copy on the new slug) reports 0 errors + 0 warnings. The v0.5.1 trademarked_term warning is gone.
  • 115 unit tests green; PHPStan max + PHP-CS-Fixer dry-run + composer audit all clean.
  • CI matrix green on PHP 8.2 / 8.3 / 8.4.

Upgrade notes

Drop the new opensalestax-for-woocommerce/ directory into wp-content/plugins/, activate, and de-activate the old opensalestax-woocommerce/ directory if you had it. Settings and tax-class mappings persist in wp_options (opensalestax_* keys) unchanged. No migration step required.

Full changelog

See CHANGELOG.md.

v0.5.1 — dual-license + WP-org Plugin Check pre-submission pass

Choose a tag to compare

@ejosterberg ejosterberg released this 16 May 00:56

Highlights

Two related changes shipped together to make v0.5.1 ready for WordPress.org plugin-directory submission.

1. Dual-license: Apache-2.0 OR GPL-2.0-or-later

Previously Apache-2.0 only. Recipient now picks:

Mirrors the Odoo connector's LGPL/AGPL pattern. Pre-empts the WP-org reviewer's "GPL compatible?" challenge.

2. WordPress.org Plugin Check pre-submission pass

From 4 ERRORS + 27 WARNINGS down to 0 ERRORS + 1 WARNING (see Known caveat below). Full Plugin Check output: specs/plugin-check/v0.5.1-final.csv.

Per-fix categories:

  • Escaping — composed-HTML output (DashboardWidget::render, OrderTaxBreakdown::renderOrderDetails) now passes through wp_kses_post(); settings table-cell renders esc_attr() every attribute; CLI fallback terminal output is esc_html'd.
  • Input sanitizationSettings::saveTaxClassMap and ConnectionTester::handle now verify nonces defensively and unslash + sanitize_text_field() every $_REQUEST / $_POST value. New unit test for the bad-nonce path.
  • Logging — every error_log() replaced with wc_get_logger()->warning(...) via a logWarning() helper; falls back to error_log only when WC isn't loaded (unit tests).
  • URL parsingUrlValidator uses wp_parse_url() instead of bare parse_url().
  • SQL annotations — direct $wpdb calls (placeholder-rate management, cache flush, dashboard widget aggregate) carry inline phpcs:ignore comments explaining the controlled-input table-name interpolation pattern. All user-supplied values bind through prepare().
  • Bootstrap hygiene — main plugin file wraps autoload in an IIFE; WC tested up to bumped 10.5 → 10.7; readme.txt Tags trimmed to the WP-org-allowed five; heredoc JS rewritten as string-concat.

Tests

  • 115 unit tests (+1 vs. v0.5.0); PHPStan max + PHP-CS-Fixer + composer audit all clean.
  • Plugin Check verified on VM 907 (WP 6.9.4 + WC 10.7 + PHP 8.4).

Known caveat — blocks WP-org submission until addressed in v0.6

The trademarked_term WARNING: WP-org's trademark policy restricts the bare "woocommerce" term in a plugin slug unless paired with one of the allowed prefixes (for woocommerce, with woocommerce, using woocommerce, and woocommerce). The WP-org reviewer will treat this as a hard reject.

The rename opensalestax-woocommerceopensalestax-for-woocommerce touches the repository name, deployment paths, text-domain string, and every __() call — deferred from v0.5.1 to its own dedicated minor.

Upgrade notes

Pure source-level cleanup + license clarification. No DB schema changes, no settings changes, no behavior changes. Drop-in upgrade.

See CHANGELOG.md for the per-fix details.

v0.5.0 — Per-state nexus filter

Choose a tag to compare

@ejosterberg ejosterberg released this 15 May 18:35

Added

  • Per-state nexus filter. Mirrors the Vendure v1.2 / Magento v1.4 sibling pattern. New admin toggle "Per-state nexus filter" + "Nexus states" text field under WooCommerce → Settings → Tax → OpenSalesTax. When enabled, the woocommerce_calc_tax handler resolves the destination state up-front and returns an empty array for any state not on the allowlist — WooCommerce's built-in tax-rate calculation handles those (typically: no tax).

Behavior

Configuration Behavior
Filter off (default) Identical to v0.4.1. Engine consulted for every taxable line.
Filter on, state in list Engine consulted as normal.
Filter on, state not in list No engine call, no tax line, WC falls back to built-in calc.
Filter on, empty list No tax anywhere (degenerate but honored).
Filter on, state unresolvable Fail-closed (no tax). Safer for an explicit opt-in.

State resolution honors the same woocommerce_tax_based_on option (billing / shipping / base) as the existing ZIP resolution.

Quality

  • 115 PHPUnit tests / 195 assertions (was 110 / 190); 5 new TaxHandler tests cover the four filter paths plus the back-compat default.
  • PHPStan max, PHP-CS-Fixer, and composer audit all clean on PHP 8.2 / 8.3 / 8.4.

Compatibility

No breaking changes. Merchants who don't toggle the filter on see identical v0.4.1 behavior.

v0.4.1 — Refund handling + engine-unreachable notice + cache regression test

Choose a tag to compare

@ejosterberg ejosterberg released this 05 May 16:00

What's new

Three small but important hardening items, all addressing real risk in the v0.4.0 codebase.

Refund handling

When you issue a refund (full or partial), the refund order's admin page now shows a prorated per-jurisdiction tax breakdown — same audit table as a regular order, with negative values.

The math: `parent_breakdown × (refund_total / parent_total) × -1`

For a $100 MN order refunded by half ($50):

Type Jurisdiction Rate Tax (refund)
state Minnesota 6.875% -$3.44
county Hennepin County 0.150% -$0.08
city Minneapolis 0.500% -$0.25
district Hennepin County Transit Sales Tax 0.500% -$0.25
district Metro Area Transportation Sales Tax 0.750% -$0.38
district Metro Area Sales and Use Tax for Housing 0.250% -$0.13
-$4.51

Verified end-to-end on VM 907 against engine v0.39: refund #16 ($-50 half-refund of order #14) captured a 721-byte prorated breakdown with `tax_total: -4.5125` (exactly half of -9.025).

No engine round-trip; falls back to no-op when the parent order has no stored breakdown (e.g., refund of an order created before v0.3.0).

Engine-unreachable admin notice

When the OpenSalesTax engine is down or misconfigured, every WP-admin page now renders a red banner telling you so:

OpenSalesTax engine is unreachable. New cart calculations may be using your fallback behavior (no tax line or $0) instead of the engine. Check the engine's status and the configured base URL. [Open settings]

Closes the silent-failure gap where a merchant who never opens WP-CLI or the dashboard widget could collect wrong tax for days without realizing the engine had been down. Capability-gated to `manage_woocommerce`. Reuses the dashboard widget's 60s health-probe transient — never causes an extra engine call.

Cache regression test

Pins the v0.3.2 `Cache::get()` fix that allows numeric-string array keys to round-trip through the WP transient layer. The original v0.1.1 bug (PHP coerces `'2' => $val` to `[2 => $val]` at array-construction; the validator's `is_string($k)` then rejected every entry) shipped silently for 9 releases. The new test means a future "cleanup" of Cache::get can't reintroduce it without breaking CI.

Quality gate

  • 109/109 unit tests pass (11 new in this release: 4 refund-handling, 6 health-notice, 2 cache regression — wait, that's 12; the math also includes 1 from the existing OrderTaxBreakdownTest infrastructure)
  • PHPStan level=max clean
  • PHP-CS-Fixer (PSR-12 + risky) clean
  • Security review re-run for v0.4.1: capability gating + output escaping on the notice; refund prorating goes through the existing breakdown validator; no new untrusted-input paths

Upgrade

Drop-in upgrade from v0.4.0 — no configuration required. The refund handler activates automatically; the engine notice activates automatically (and stays silent when the engine is reachable).

```bash
cd wp-content/plugins/opensalestax-woocommerce
git pull
composer install --no-dev
```

Full changelog

See CHANGELOG.md.

v0.4.0 — WooCommerce Subscriptions integration

Choose a tag to compare

@ejosterberg ejosterberg released this 05 May 05:46

What's new

Auto-recalculate tax on every WooCommerce Subscriptions renewal.

The problem this solves

WooCommerce Subscriptions, by default, copies the parent subscription's original tax line onto every renewal order. That works fine on day one — but a year later, when state law has changed, transit-district rates have moved, or the customer has relocated, the renewal still bills the original rate. You silently under-collect (or over-collect) for months until someone notices.

What v0.4.0 does

A new `SubscriptionsBridge` hooks `wcs_renewal_order_created` (priority 20) and forces a fresh tax recalculation:

  1. WC's `calculate_taxes()` walks the renewal's line items
  2. That triggers `woocommerce_calc_tax`
  3. Which lands in our `TaxHandler`
  4. Which calls the engine with today's rates
  5. Renewal order saved with correct tax

The per-jurisdiction breakdown is also captured on the renewal so the admin order-edit page shows the same audit table as a regular order.

When it activates

The bridge is gated on `class_exists('WC_Subscriptions')`. If WC Subscriptions isn't installed, the hook is never registered — zero performance or behavior impact for the vast majority of stores.

If WC Subscriptions IS installed, the bridge activates automatically. No configuration toggle.

Failure handling

Recalc failures (engine unreachable, network issue, etc.) are caught and logged to the PHP error log; the renewal proceeds with whatever tax was inherited. Better to be a few cents off than to block a renewal payment.

⚠️ Live testing limitation

Live verification against the actual WC Subscriptions plugin (a paid $199/yr product) is deferred to merchants and contributors who own it. The integration is built against the documented `wcs_renewal_order_created` hook signature and verified with stubs in CI. Real-world signal is welcomed via GitHub issues — particularly:

  • Address scenarios where the customer's destination changes mid-subscription
  • Edge cases around tax-exempt customers becoming non-exempt (or vice-versa)
  • Rate-change behavior across renewal boundaries

Quality gate

  • 98/98 unit tests pass (7 new in `SubscriptionsBridgeTest`: detection-not-installed / register no-op / detection-active / register-hooks / recalc-method-call-sequence / recalc-survives-errors / recalc-skips-non-object)
  • PHPStan level=max clean
  • PHP-CS-Fixer (PSR-12 + risky) clean
  • Security review re-run for v0.4.0: hook gating ✓, defensive type-checks on hook payload ✓, zero surface change for installs without WC Subscriptions ✓

Upgrade

Drop-in upgrade from v0.3.3 — no configuration required. The bridge auto-activates if WC Subscriptions is present and is invisible otherwise.

```bash
cd wp-content/plugins/opensalestax-woocommerce
git pull
composer install --no-dev
```

Full changelog

See CHANGELOG.md.

v0.3.3 — Admin-UI tax-class mapper

Choose a tag to compare

@ejosterberg ejosterberg released this 05 May 05:40

What's new

Configure tax-class mappings without leaving the browser. The CLI commands from v0.2.0 still work, but now there's a UI for the same thing under WC > Settings > Tax > OpenSalesTax → Tax class → OST category mapping.

What it looks like

WC tax class OST category Status
Standard (empty slug) [general ▾] Default
reduced-rate [general ▾] Default
zero-rate [— Skip — ▾] Default
clothing [clothing ▾] Custom
gift-cards [— Skip — ▾] Custom

The dropdown shows the 6 OST categories plus "— Skip (non-taxable) —" so a merchant can mark anything from a custom class to a single product line as non-taxable. The Status column flags which rows are merchant overrides vs. plugin defaults.

A "Reset all to defaults" checkbox clears every override on save.

Auto-discovery

The mapper finds every WC tax class to show a row for, automatically:

  • WC's 4 built-in slugs (`''`, `standard`, `reduced-rate`, `zero-rate`)
  • Any user-defined classes (read from `woocommerce_tax_classes` and slugified via `sanitize_title` the same way WC does)
  • Anything already present in the merchant's existing override map

So if you've got a "Clothing" class defined in WC > Settings > Tax > Tax options, it shows up in the OpenSalesTax mapper automatically.

Security

  • Capability check: `current_user_can('manage_woocommerce')` runs before any state change. Both reset and persist paths gated.
  • Server-side validation: posted categories pass through `wp_unslash` + `sanitize_text_field` and validate against the `VALID_CATEGORIES` allow-list. Invalid values are silently dropped — the dropdown shouldn't produce them, but defense-in-depth catches a tampered-form submission.
  • Verified live on VM 907: admin user's POST persists, a 3-entry round-trips correctly, reset clears, an unprivileged user's POST is no-op'd.

Quality gate

  • 91/91 unit tests pass (6 new in `SettingsTaxClassSaveTest`: capability rejects / reset path / no-op when no map / valid persist / invalid silent-drop / all 6 valid categories accepted)
  • PHPStan level=max clean
  • PHP-CS-Fixer (PSR-12 + risky) clean

Upgrade

Drop-in upgrade from v0.3.2 — no configuration required. Existing override maps from prior CLI configuration carry through unchanged.

```bash
cd wp-content/plugins/opensalestax-woocommerce
git pull
composer install --no-dev
```

Full changelog

See CHANGELOG.md.

v0.3.2 — Recent-calculations debug log + cache fix

Choose a tag to compare

@ejosterberg ejosterberg released this 05 May 05:34

What's new

Recent-calculations debug log — a 50-entry ring buffer that captures every cart tax calculation so merchants can answer "why is this tax $8.25 when it should be $9.03?" without grepping the server's PHP error log.

Each entry records:

  • Timestamp (UTC, ISO-8601)
  • Source: `cache-hit` / `engine-call` / `error`
  • Destination ZIP, OST category, pre-tax amount
  • Computed tax total
  • Round-trip duration (ms) for engine calls
  • Error message + class name for failures

Disabled by default — adds one option-write per calculation, so leave it OFF in production unless investigating an issue.

How to use

Toggle: WC > Settings > Tax > OpenSalesTax → "Calculation log" → Enabled.

Or via WP-CLI:
```bash
wp option update opensalestax_calc_log_enabled 1
```

View:

  • Settings page: scroll below the form for the "Recent calculations" panel.
  • WP-CLI: `wp opensalestax recent-calcs` (with `--limit` flag).
  • Programmatic: `OpenSalesTax\WooCommerce\CalculationLog::getRecent()`.

Clear:
```bash
wp opensalestax clear-log
```

🐞 Pre-existing cache bug fixed

While instrumenting the log paths, I found that Cache::get() was silently degrading to "no caching" since v0.1.1. PHP auto-converts numeric-string array keys to `int` when an array is constructed with `[$key => $val]`, but `Cache::get()`'s validator rejected entries where any key wasn't `is_string()`. So when the placeholder rate ID (a stringified int) round-tripped through the transient layer, it came back as int and the cache layer rejected it.

Result: every cart calculation hit the engine, every time. Performance hit but no correctness issue.

Now fixed. Verified end-to-end on VM 907 against engine v0.39: call 1 → `engine-call` (241ms), call 2 → `cache-hit` (no engine round-trip). Caching now actually caches.

Quality gate

  • 85/85 unit tests pass (10 new in `CalculationLogTest`)
  • PHPStan level=max clean
  • PHP-CS-Fixer (PSR-12 + risky) clean
  • Security review re-run for v0.3.2: deserialization safety verified (loadRaw rejects non-array roots and entries); output escaping on the settings-page log viewer passes every value through `esc_html()` via a `stringify` helper

Upgrade

Drop-in upgrade from v0.3.1 — no configuration required. The cache bug fix means the engine will see significantly fewer requests once cached entries start landing.

```bash
cd wp-content/plugins/opensalestax-woocommerce
git pull
composer install --no-dev
```

Full changelog

See CHANGELOG.md.

v0.3.1 — Status dashboard widget

Choose a tag to compare

@ejosterberg ejosterberg released this 05 May 05:25

What's new

OpenSalesTax health at-a-glance. The WP-admin home page now shows a compact dashboard widget below "At a Glance" with four signals merchants want to see every time they log in.

What the widget shows

Signal Possible values
Connection OK (engine vX.Y, DB connected) / Unreachable / Not configured
Placeholder rate OK (tax_rate_id = N) / Missing — re-activate the plugin
Orders today count of orders with engine breakdown captured today
Quick actions Configure (settings page) + View orders

Implementation

  • New class `OpenSalesTax\WooCommerce\DashboardWidget`
  • Hooks `wp_dashboard_setup`; visibility gated on `manage_woocommerce` capability
  • Health probe response cached 60s in a transient (`opensalestax_dashboard_health`) — the widget never hammers the engine
  • HPOS-aware: queries `wp_wc_orders_meta` first, falls back to `wp_postmeta` for legacy CPT installs
  • All output escaped via `esc_html` / `esc_attr` / `esc_url`
  • All SQL via `$wpdb->prepare()` with `%s` placeholders

Verified end-to-end

On VM 907 against engine v0.36, the live `renderHtml()` produces a 1070-byte panel showing OK status, engine version, configured base URL, and per-day order count. Renders correctly under the capability check.

Quality gate

  • 74/74 unit tests pass (5 new in `DashboardWidgetTest`: not-configured / healthy / unreachable / missing-placeholder / cached-health)
  • PHPStan level=max clean
  • PHP-CS-Fixer (PSR-12 + risky) clean
  • Security review re-run for v0.3.1: capability check on `addWidget` ✓, SQL injection safe via `$wpdb->prepare(%s)` ✓, output escaping on every interpolated value ✓

Upgrade

Drop-in upgrade from v0.3.0 — no configuration required. The widget appears automatically on next admin page-load for users with `manage_woocommerce`.

```bash
cd wp-content/plugins/opensalestax-woocommerce
git pull
composer install --no-dev
```

Full changelog

See CHANGELOG.md.