Woo Package Shipping
A WooCommerce shipping method that calculates costs based on the number of packages required and applies a configurable duties percentage on the cart subtotal.
by Lorenzo Accorinti · github.com/accolore/woo-package-shipping · website
Install
No release zip yet. The repository archive installs, but the folder name will carry the branch suffix and updates will not flow:
wp plugin install https://github.com/accolore/woo-package-shipping/archive/refs/heads/main.zipA WooCommerce shipping method that calculates costs based on the number of packages required and applies a configurable duties percentage on the cart subtotal. The method is restricted to a single, admin-selected destination country and supports a free-shipping threshold above which both shipping and duties are waived.
Requirements
| Dependency | Minimum version |
|---|---|
| PHP | 7.4 |
| WordPress | 5.9 |
| WooCommerce | 7.0 |
Installation
- Copy the
woo-package-shippingfolder intowp-content/plugins/. - In the WordPress admin go to Plugins and activate Woo Package Shipping.
- Navigate to WooCommerce → Settings → Shipping and create or open a shipping zone.
- Click Add shipping method, select Package Shipping with Duties and click Add shipping method.
- Click the method name to open its settings and configure the fields described below.
Configuration
All settings are per shipping-zone instance and are available in the method's settings panel.
| Field | Default | Description |
|---|---|---|
| Method title | Package Shipping + Duties | Label displayed to the customer at checkout. |
| Destination country | (none) | The method is shown only when the shipping address matches this country. Must be selected; leaving it empty disables the method. |
| Free shipping threshold (€) | 1500.00 | Orders whose subtotal (after discounts, before shipping) meets or exceeds this amount qualify for free shipping. Both the per-package cost and duties are waived. |
| Cost per package (€) | 170.00 | Fixed shipping cost charged per package dispatched. |
| Items per package | 20 | Maximum number of units that fit in one package. The 21st item starts a new package. Must be ≥ 1. |
| Duties percentage (%) | 15 | Percentage of the cart subtotal added as duties. Applied only when free shipping does not apply. Must be between 0 and 100. |
Pricing logic
When the order is below the free-shipping threshold
packages = ceil( total_quantity / items_per_package )
shipping_cost = packages × cost_per_package
duties_cost = subtotal × ( duties_percent / 100 )
total = shipping_cost + duties_cost
Examples (defaults: 170 €/pkg, 20 items/pkg, 15% duties, threshold 1500 €):
| Cart qty | Subtotal | Packages | Shipping | Duties (15%) | Total |
|---|---|---|---|---|---|
| 1 | 200 € | 1 | 170 € | 30 € | 200 € |
| 20 | 1 000 € | 1 | 170 € | 150 € | 320 € |
| 21 | 1 000 € | 2 | 340 € | 150 € | 490 € |
| 40 | 1 200 € | 2 | 340 € | 180 € | 520 € |
| 41 | 1 200 € | 3 | 510 € | 180 € | 690 € |
When the order meets or exceeds the free-shipping threshold
shipping_cost = 0
duties_cost = 0
total = 0
The checkout label reads: <Method title> (Free).
Checkout rate label
When shipping is not free, the label shown at checkout summarises the breakdown:
Package Shipping + Duties (2 pkg × €340.00 + €150.00 duties)
This lets the customer see exactly how many packages are being dispatched and what the duties component is.
File structure
woo-package-shipping/
├── woo-package-shipping.php ← Plugin entry point
├── includes/
│ ├── class-package-shipping-calculator.php ← Pure calculation logic (no WP dependencies)
│ ├── class-wcml-currency-helper.php ← WCML Multicurrency conversion helper
│ └── class-wc-shipping-package-duties.php ← WC_Shipping_Method implementation
├── tests/
│ └── test-calculator.php ← Standalone CLI test suite
├── plan.md ← Design plan and architecture notes
└── docs.md ← Original requirements
woo-package-shipping.php
Plugin entry point. Declares HPOS compatibility, checks that WooCommerce is active, loads the two class files, and registers the shipping method via the woocommerce_shipping_methods filter.
includes/class-package-shipping-calculator.php
Stateless utility class with no WordPress or WooCommerce dependencies. All arithmetic lives here to keep it easily testable in isolation.
| Method | Description |
|---|---|
should_free_shipping( $subtotal, $threshold ) |
Returns true when $subtotal >= $threshold. |
count_packages( $total_qty, $items_per_package ) |
Returns ceil( $total_qty / $items_per_package ). Returns 0 if either argument is ≤ 0. |
compute_shipping_cost( $packages, $cost_per_package ) |
Returns $packages × $cost_per_package. Returns 0.0 if either argument is ≤ 0. |
compute_duties( $subtotal, $percent, $shipping_is_free ) |
Returns $subtotal × ($percent / 100). Returns 0.0 when $shipping_is_free is true. |
calculate( $total_qty, $subtotal, $threshold, $cost_per_package, $items_per_package, $duties_percent ) |
Aggregates all of the above and returns an array with keys is_free, packages, shipping_cost, duties_cost, total_cost. |
includes/class-wc-shipping-package-duties.php
Extends WC_Shipping_Method. Responsibilities:
init_form_fields()— Defines the admin settings fields listed in the Configuration section.process_admin_options()— Overrides the parent to clamp values out of range and reject unknown country codes after saving.is_available( $package )— Returnstrueonly when the method is enabled, a country is configured, and the package destination matches.calculate_shipping( $package )— Reads settings, converts monetary amounts viaWCML_Currency_Helper, sums cart quantities from$package['contents'], reads the subtotal from$package['contents_cost'], delegates arithmetic toPackage_Shipping_Calculator::calculate(), then callsadd_rate().
includes/class-wcml-currency-helper.php
Single-purpose helper for WCML Multicurrency integration. See the section below for details.
WCML Multicurrency integration
When WooCommerce Multilingual & Multicurrency (WCML) is installed and its Multicurrency feature is enabled, the plugin automatically converts the two monetary settings — Cost per package and Free shipping threshold — to the customer's active currency before performing any calculation.
How it works
WCML exposes the wcml_raw_price_amount filter (available since WCML 3.5). The plugin calls this filter inside WCML_Currency_Helper::convert():
$converted = apply_filters( 'wcml_raw_price_amount', $price );
WCML hooks into this filter and returns the value converted according to the exchange rate configured in WooCommerce → WPML Multicurrency → Currencies.
| Value | Stored in | At runtime |
|---|---|---|
| Cost per package | Default store currency | Converted to active currency |
| Free shipping threshold | Default store currency | Converted to active currency |
| Duties percentage | — (dimensionless) | No conversion needed |
Cart subtotal (contents_cost) |
Already in active currency | Used as-is |
Graceful fallback
When WCML is not installed, has Multicurrency disabled, or has no exchange rate configured, the wcml_raw_price_amount filter returns the original value unchanged. The plugin therefore works correctly in single-currency stores with no configuration changes.
Checking whether WCML is active
WCML_Currency_Helper::is_active(); // returns true when wcml_raw_price_amount has a listener
Running the tests
The test suite is a self-contained CLI script with no external dependencies:
G:\xampp\php\php.exe tests/test-calculator.php
Expected output (31 assertions):
[PASS] Free shipping at exact threshold
[PASS] Free shipping above threshold
...
Results: 31 passed, 0 failed.
Security notes
- All option values are read through WooCommerce's own
get_option()/update_option()API; no raw$_POSTaccess occurs in the shipping logic. - Output is escaped with
esc_html()where applicable. - The destination country is validated against WooCommerce's full country list before being persisted.
- No custom AJAX endpoints or database tables are introduced.
License
GPL-2.0-or-later — see https://www.gnu.org/licenses/gpl-2.0.html.