Install
This is the repository archive. It installs, but the folder name will carry the branch suffix and updates will not flow.
Install straight from GitHub with WP-CLI:
wp plugin install https://github.com/nvdigitalsolutions/nv-boxpacker/archive/refs/heads/main.zipUSPS Priority Shipping Optimizer
A WooCommerce plugin that optimizes USPS Priority Mail shipping for WooCommerce orders by packing items into custom cubic boxes and USPS flat-rate boxes, rate-shopping through ShipEngine or ShipStation, and producing package-level plans suitable for PirateShip export.
Author: NV Digital Solutions License: GPLv3 or later — see LICENSE for details.
Table of Contents
- Overview
- Requirements
- Installation
- Configuration
- Features
- Architecture
- WordPress Hooks and Filters
- Box Definition JSON Schema
- Development
- Changelog
Overview
When a customer completes checkout, this plugin:
- Collects all shippable items from the WooCommerce order.
- Packs them using the dvdoug/boxpacker library (falls back to a single-item-per-box strategy when the library is unavailable).
- For each configured carrier/service pair, evaluates all packed boxes and picks the cheapest rate from ShipEngine, ShipStation, or both. Each service is offered as a separate shipping option at checkout (e.g. "USPS Priority $7.25" and "UPS Ground $8.50").
- Stores a complete, package-level shipping plan on the order for admin review.
- Generates PirateShip-compatible CSV rows for bulk label purchase.
Managers can also use the WooCommerce → USPS Test Pricing admin page to preview packing and rates for arbitrary items and addresses without placing a real order.
Requirements
| Dependency | Minimum version |
|---|---|
| PHP | 8.0 |
| WordPress | 6.0 |
| WooCommerce | 7.0 |
| dvdoug/boxpacker | ^3.12 (optional; installed via Composer) |
Installation
Production
- Download or build the plugin zip (see
bin/build.sh). - Upload and activate through Plugins → Add New → Upload Plugin in WordPress, or extract to
wp-content/plugins/fk-usps-optimizer/. - Open WooCommerce → USPS Optimizer to complete configuration.
Note:
vendor/is excluded from the repository. Runcomposer install --no-devinside the plugin directory before deploying, or use the build script which handles this automatically.
Development
# Clone the repository
git clone https://github.com/nvdigitalsolutions/nv-boxpacker.git
cd nv-boxpacker
# Install all dependencies including dev tools
composer install
# Verify the setup
composer test # PHPUnit
composer phpcs # PHP CodeSniffer (WPCS)
Configuration
Navigate to WooCommerce → USPS Optimizer to manage all plugin settings.
Enabled Carrier APIs
| Setting | Values | Default |
|---|---|---|
| Enabled Carrier APIs | ShipEngine, ShipStation (checkboxes) |
ShipEngine |
Enable one or more carrier APIs for live rate requests. When multiple carriers are enabled, the plugin fetches rates from all of them and uses the cheapest option. Checking or unchecking a carrier immediately shows or hides its credential fields without saving the page.
ShipEngine Settings
| Setting | Description |
|---|---|
| ShipEngine API Key | API key from app.shipengine.com → API Management. |
| ShipEngine Carrier ID | The carrier ID for your USPS account (e.g. se-123456). Found in Carriers on the ShipEngine dashboard. |
| ShipEngine Service Code | Service code sent to ShipEngine for rate requests (e.g. usps_priority_mail). Default: usps_priority_mail. |
ShipEngine uses a header-based API-Key authentication scheme and calls the POST /v1/rates endpoint.
ShipStation Settings
| Setting | Description |
|---|---|
| ShipStation API Key | API key from ShipStation → Account Settings → API Settings. |
| ShipStation API Secret | API secret from the same page. |
| ShipStation Carrier Code | Primary carrier code to rate against (e.g. stamps_com, ups_walleted). Default: stamps_com. |
| ShipStation Service Code | Service code sent to ShipStation for rate requests (e.g. usps_priority_mail). Default: usps_priority_mail. Leave empty to match any service from the carrier. |
| ShipStation Additional Services | Optional JSON array of extra carrier+service pairs to rate-shop alongside the primary pair. Each entry needs carrier_code and service_code. Example: [{"carrier_code":"ups_walleted","service_code":"ups_ground"}]. Rates from all pairs are compared and the cheapest wins. |
ShipStation uses HTTP Basic Authentication (API Key:API Secret) and calls the GET /shipments/getrates endpoint.
You can override the ShipStation API base URL using the
fk_usps_optimizer_shipstation_api_urlfilter (see Filter Hooks).
Service Codes
Each carrier now has its own service code setting. This replaces the previous shared USPS Service Code field.
| Setting | Carrier | Default |
|---|---|---|
| ShipEngine Service Code | ShipEngine | usps_priority_mail |
| ShipStation Service Code | ShipStation | usps_priority_mail |
Common service codes include usps_priority_mail, usps_first_class_mail, and usps_ground_advantage. The legacy shared service_code setting is still respected as a fallback when per-carrier codes are empty (backward compatibility).
Display Settings
These settings control how shipping rates appear in the WooCommerce cart and checkout.
| Setting | Type | Default | Description |
|---|---|---|---|
| Show All Options | Checkbox | Off | Display every combination of rated box candidates as a separate shipping option. Candidates are grouped per carrier service — the cartesian product runs within each service, not across services, preventing nonsensical cross-service combinations. Labels use the carrier-specific service name (e.g. "USPS Priority", "UPS Ground") derived from each plan's actual API-returned service code. Repeated box names are consolidated (e.g. "2× Small Flat Rate Box + Large Flat Rate Box"). When disabled, only the cheapest combined rate per service is shown. |
| Show Package Count | Checkbox | Off | Append the package count to each shipping option label. Example: "USPS Priority (2 packages)". Uses proper singular/plural forms. |
| Show Estimated Delivery Date | Checkbox | Off | Show the carrier-provided estimated delivery date on a separate line below each shipping option label. Example: "USPS Priority (1 package)" on the first line, "Est. delivery: Mon, Jan 15" on the second. For ShipEngine, the estimated_delivery_date field from the rate response is used directly. For ShipStation, the transitDays field is converted to a calendar date. The formatted date is also passed as WooCommerce rate metadata for themes and FunnelKit Checkout pages that render it. |
| Additional Business Days | Number (0–30) | 0 | Extra business days (Monday–Friday) added to every estimated delivery date. Use this to account for order processing or handling time. Weekends are skipped, so a 2-business-day buffer applied on a Thursday pushes the estimate to the following Monday. Applies to both carrier-returned and default transit-day estimates. |
| PirateShip Notification Emails | Text | empty | Comma-separated list of email addresses (newlines and semicolons also accepted) to notify after every order. Each recipient receives the suggested packages, packing list, and a CSV attachment that can be imported directly into PirateShip. Invalid addresses are dropped on save and reported via an admin notice. Leave blank to disable. |
Test ShipEngine Connection
Below the settings form a Test Connection button verifies that:
- The configured ShipEngine API Key authenticates successfully against the ShipEngine API.
- The configured Carrier ID belongs to an active USPS carrier account (
stamps_com,usps, orendicia).
Clicking the button fires an AJAX request — the result is displayed inline immediately below the button without reloading the page. Save your settings before running this test so the current values are used.
Tip: For sandbox testing, enter a
TEST_-prefixed API key from your ShipEngine dashboard and enable Sandbox Mode above.
Sandbox Mode
When Enable Sandbox Mode is checked:
- All ShipStation and ShipEngine log entries are prefixed with
[SANDBOX]so they are distinguishable from production calls. - A yellow warning banner is displayed on the USPS Test Pricing admin page.
- The test pricing results table shows a notice that rates are from a sandbox environment.
Use sandbox mode during development and testing to clearly mark non-production API calls in the WooCommerce log viewer.
Ship-From Address
| Field | Description |
|---|---|
| Ship From Name | Sender full name. |
| Ship From Company | Company name (optional). |
| Ship From Phone | Sender phone number. |
| Ship From Address 1 | Street address. |
| Ship From Address 2 | Suite/unit (optional). |
| Ship From City | City. |
| Ship From State | Two-letter state abbreviation (e.g. CA). |
| Ship From Postal Code | ZIP code. |
| Ship From Country | Two-letter country code. Default: US. |
The ship-from address is passed directly to both ShipEngine and ShipStation for rate calculations. It can also be overridden via the fk_usps_optimizer_ship_from_address filter.
Debug Logging
When Enable Debug Logging is checked, all API requests, responses, and packing errors are written to the WooCommerce logger under the fk-usps-optimizer source. View logs at WooCommerce → Status → Logs.
Box Definitions
Box definitions are managed via a compact visual table in the Box Definitions section of the settings page. Each row represents one physical box and can be added, edited, or removed directly in the UI. The table uses grouped column headers — "Outer (in)" and "Inner (in)" — with fixed-width columns and a horizontally scrollable wrapper so it fits within the standard WordPress admin layout without forcing the page wider.
Each box has inner/outer dimensions (inches), empty weight (ounces), maximum payload weight (lbs), a type (cubic or flat_rate), and an optional Carrier restriction (Any, USPS, UPS, FedEx). The Enabled checkbox lets you temporarily exclude a box from packing/rating (for example, when a box is out of stock) without deleting its configuration — uncheck it to disable, re-check it when stock is replenished. Boxes saved before this setting existed are treated as enabled by default.
See Box Definition JSON Schema for the full field reference and a worked example.
Features
WooCommerce Shipping Zones Integration
The plugin registers a native WC_Shipping_Method (fk_usps_optimizer) so it appears in WooCommerce → Settings → Shipping → Shipping Zones. Add the USPS Priority Optimizer method to any zone to enable live, optimized USPS Priority rates during cart and checkout.
How it works:
- Cart items are extracted and converted to the item format expected by
Packing_Service::pack_items(). - Items without WooCommerce product dimensions (length, width, or height not set) are detected and packed individually via the fallback packer — one item per box. This prevents the BoxPacker default of 1×1×1 inch dimensions from producing incorrect packing results.
- Each configured carrier/service pair rates all packed packages independently. The cheapest rate per service is offered as a separate checkout option — customers see one shipping rate per service (e.g. "USPS Priority $7.25" and "UPS Ground $8.50" as distinct choices). Service labels are derived from the actual API-returned
serviceCode, not the configured setting, ensuring accuracy even when the carrier returns a different service than requested. - Rates are cached in a transient for 30 minutes. The cache key includes carrier, service code, box configuration, display settings, item dimensions, and destination — so rates update immediately when any of these change.
When Show All Options is enabled, each rate option shows a descriptive label built from the carrier service name and the box names in that combination, such as "USPS Priority — 2× Small Flat Rate Box + Large Flat Rate Box" or "UPS Ground — 2× Bag". The cartesian product is grouped per carrier service — candidates from different services are never mixed. Repeated box names are consolidated (e.g. "2× Small" instead of "Small + Small"). Duplicate combinations are removed and results are sorted cheapest-first.
Automatic Order Planning
On woocommerce_checkout_order_processed (priority 20), the plugin:
- Collects shippable items via
Packing_Service::pack_order(). - Packs them using BoxPacker (or fallback).
- Rate-shops every packed package across all enabled carrier services and picks the cheapest rate.
- Saves the result to the order meta key
_fk_usps_shipping_planas a serialized PHP array. - Displays the plan on the order detail page under USPS Shipping Plan.
The plan structure is:
[
'created_at' => '2024-01-15 12:00:00', // UTC
'total_package_count' => 2,
'total_rate_amount' => 14.65,
'currency' => 'USD',
'packages' => [ /* see Package Plan below */ ],
'pirateship_rows' => [ /* CSV row arrays */ ],
'warnings' => [ /* non-fatal notices */ ],
]
Package Plan (one entry per packed box):
[
'package_number' => 1,
'mode' => 'cubic', // 'cubic', 'flat_rate_box', or 'package'
'package_code' => 'package',
'package_name' => 'Custom Cubic Small',
'service_code' => 'usps_priority_mail',
'rate_amount' => 7.45,
'currency' => 'USD',
'weight_oz' => 18.5,
'dimensions' => ['length' => 8, 'width' => 8, 'height' => 6],
'cubic_tier' => '0.2', // empty string for flat-rate
'packing_list' => ['2x Widget', '1x Gadget'],
'items' => [ /* raw item arrays */ ],
]
Admin Test Pricing Page
Navigate to WooCommerce → USPS Test Pricing to use the test pricing tool.
How it works:
- Enter one or more items with name, quantity, dimensions (inches), and weight (ounces).
- Enter the destination address.
- Click Run Test Pricing.
The plugin packs the items and fetches live rates from all enabled carriers. Results are displayed per package and include:
- Estimated rate (formatted as WooCommerce price)
- Service code
- Dimensions and weight
- Cubic tier (when applicable)
- Packing list
Rows can be added dynamically with the + Add Item button and removed with the Remove button next to each row. The form repopulates with submitted values so you can adjust and re-run without re-entering data.
Sandbox warning: If Sandbox Mode is enabled, a yellow banner appears at the top of the page and the results table shows a note that rates come from a sandbox environment.
PirateShip CSV Export
From the order detail page, click Export to PirateShip to download a CSV pre-formatted for bulk import into PirateShip. Each row corresponds to one package in the shipping plan.
Architecture
Class Reference
| Class | File | Responsibility |
|---|---|---|
Plugin |
includes/class-plugin.php |
Singleton bootstrap. Instantiates all services, registers the plugins_loaded init hook, registers the WooCommerce shipping method, and wires woocommerce_checkout_order_processed. Compares rates across all enabled carriers via get_carrier_services(). |
Settings |
includes/class-settings.php |
Reads/writes all plugin options via get_option/register_setting. Provides typed getters for every setting including get_carriers(), per-carrier service code getters (get_shipengine_service_code(), get_shipstation_service_code()), get_shipstation_service_pairs(), and get_boxes_for_carrier() for carrier-filtered box retrieval. Renders the settings admin page with a visual box management table. |
Shipping_Method |
includes/class-shipping-method.php |
Extends WC_Shipping_Method. Provides live shipping rates in WooCommerce shipping zones. Extracts cart items, packs them, rate-shops via all enabled carrier services, and handles "Show All Options" via cartesian product of rated box candidates per package, with rate caching. |
Packing_Service |
includes/class-packing-service.php |
Collects shippable items from a WC_Order, packs them with dvdoug/boxpacker (or a simple fallback), and returns normalized package arrays. pack_items() accepts an optional $boxes parameter to override the configured box definitions. Items without dimensions are separated and packed individually via fallback. Packed package dimensions use inner box dimensions. |
ShipEngine_Service |
includes/class-shipengine-service.php |
Rate-shops packed packages against the ShipEngine v1 API (POST /v1/rates). Supports both order-based and address-based rate requests. |
ShipStation_Service |
includes/class-shipstation-service.php |
Rate-shops packed packages against the ShipStation API (GET /shipments/getrates) using HTTP Basic Auth. Mirrors the ShipEngine interface. Supports carrier/service code overrides for multi-pair rate shopping. Prepends [SANDBOX] to log messages when sandbox mode is active. |
Test_Pricing_Service |
includes/class-test-pricing-service.php |
Accepts raw form items, expands them by quantity, packs them, and delegates to all enabled carrier services. Returns a results array consumed by Admin_Test_UI. |
Order_Plan_Service |
includes/class-order-plan-service.php |
Reads and writes the _fk_usps_shipping_plan order meta. |
PirateShip_Export |
includes/class-pirateship-export.php |
Generates PirateShip CSV rows from package plans and streams the CSV download. |
Admin_UI |
includes/class-admin-ui.php |
Renders the shipping plan on the WooCommerce order detail page. |
Admin_Test_UI |
includes/class-admin-test-ui.php |
Renders the WooCommerce → USPS Test Pricing submenu page. Handles nonce-verified form POST, parses and sanitizes submitted data, and displays per-package results. |
BoxPacker_Box |
includes/class-boxpacker-box.php |
Adapts the plugin's box definitions to the dvdoug/boxpacker BoxInterface. |
BoxPacker_Item |
includes/class-boxpacker-item.php |
Adapts item arrays to the dvdoug/boxpacker ItemInterface. Carries the original item array as metadata. |
WordPress Hooks and Filters
Action Hooks
| Hook | Priority | Description |
|---|---|---|
plugins_loaded |
default | Calls Plugin::init() to check WooCommerce is active and register all sub-hooks. |
admin_init |
default | Registers settings fields via Settings::register_settings(). |
admin_menu |
default | Adds USPS Optimizer and USPS Test Pricing submenu pages under WooCommerce. |
admin_enqueue_scripts |
default | Enqueues assets/js/settings.js and localizes ajaxUrl, a nonce, and i18n strings on the settings page only. |
woocommerce_shipping_methods |
default (filter) | Registers Shipping_Method (fk_usps_optimizer) so it appears in WooCommerce shipping zones. |
woocommerce_checkout_order_processed |
20 | Triggers order packing and rate-shopping via Plugin::process_order(). |
wp_ajax_fk_usps_test_connection |
default | AJAX handler for the settings page Test Connection button. Verifies the nonce, calls ShipEngine_Service::test_connection(), and returns a JSON response so the result renders inline without a page reload. |
Filter Hooks
All filters follow the naming convention fk_usps_optimizer_{name}.
| Filter | Default | Description |
|---|---|---|
fk_usps_optimizer_carriers |
['shipengine'] |
Override the array of enabled carrier identifiers. Return an array containing 'shipengine' and/or 'shipstation'. |
fk_usps_optimizer_carrier |
'shipengine' |
Override the primary carrier (first enabled). Return 'shipengine' or 'shipstation'. Kept for backward compatibility. |
fk_usps_optimizer_boxes |
(saved JSON) | Modify or replace the array of box definitions at runtime before packing. |
fk_usps_optimizer_ship_from_address |
(settings value) | Override the ship-from address array. Useful for multi-warehouse setups. |
fk_usps_optimizer_shipengine_api_key |
(settings value) | Override the ShipEngine API key at runtime (e.g. from environment variable). |
fk_usps_optimizer_shipengine_carrier_id |
(settings value) | Override the ShipEngine carrier ID at runtime. |
fk_usps_optimizer_shipengine_service_code |
'usps_priority_mail' |
Override the ShipEngine service code at runtime. |
fk_usps_optimizer_shipstation_api_key |
(settings value) | Override the ShipStation API key at runtime. |
fk_usps_optimizer_shipstation_api_secret |
(settings value) | Override the ShipStation API secret at runtime. |
fk_usps_optimizer_shipstation_carrier_code |
'stamps_com' |
Override the ShipStation carrier code at runtime. |
fk_usps_optimizer_shipstation_service_code |
'usps_priority_mail' |
Override the ShipStation service code at runtime. |
fk_usps_optimizer_shipstation_service_pairs |
(primary + additional) | Override the array of ShipStation carrier+service pairs used for rate shopping. Each entry is an associative array with carrier_code and service_code. |
fk_usps_optimizer_shipstation_api_url |
'https://ssapi.shipstation.com' |
Override the ShipStation API base URL (useful for mocking in tests). |
fk_usps_optimizer_api_timeout |
8 |
Per-request timeout (seconds) for ShipEngine/ShipStation HTTP calls. Receives the carrier slug ('shipengine' or 'shipstation'). |
fk_usps_optimizer_max_candidates |
3 |
Maximum number of rated candidate boxes per packed package. Receives the full candidate array as context. Lower values trade rate variety for fewer API calls. |
fk_usps_optimizer_rate_cache_ttl |
300 |
TTL (seconds) for the rate-response transient cache. Set to 0 to disable caching. Sandbox endpoints bypass the cache regardless of this value. |
fk_usps_optimizer_skip_rates |
false |
When truthy, Shipping_Method::calculate_shipping() returns immediately. Receives the WooCommerce shipping package as context. Useful as a feature flag or debug toggle. |
fk_usps_optimizer_min_postcode_length |
US/PR = 5, CA = 3, default = 3 |
Minimum postcode length below which rate calculation is skipped (prevents API churn during partial-checkout typing). Receives the default int and the uppercased country code. Return 0 to disable the gate. |
fk_usps_optimizer_intl_service_code_map |
(see source) | Override the domestic-to-international service code mapping used when the destination country is not US. Receives an associative array of domestic service codes (keys) to international equivalents (values). Covers USPS, UPS, and FedEx. |
Example — load credentials from environment variables:
add_filter( 'fk_usps_optimizer_shipengine_api_key', function () {
return getenv( 'SHIPENGINE_API_KEY' ) ?: '';
} );
add_filter( 'fk_usps_optimizer_shipstation_api_key', function () {
return getenv( 'SHIPSTATION_API_KEY' ) ?: '';
} );
Example — add a custom box definition:
add_filter( 'fk_usps_optimizer_boxes', function ( array $boxes ): array {
$boxes[] = [
'reference' => 'My Custom Box',
'package_code' => 'package',
'package_name' => 'My Custom Box',
'box_type' => 'cubic',
'outer_width' => 10,
'outer_length' => 10,
'outer_depth' => 10,
'inner_width' => 10,
'inner_length' => 10,
'inner_depth' => 10,
'empty_weight' => 4,
'max_weight' => 20,
'carrier_restriction' => '',
];
return $boxes;
} );
Box Definition JSON Schema
Box definitions are stored in the plugin settings as a JSON array. Each element is an object with the following fields:
| Field | Type | Description |
|---|---|---|
reference |
string | Human-readable label shown in admin UI and logs. |
package_code |
string | Carrier package code sent to the API (e.g. package, small_flat_rate_box). |
package_name |
string | Display name shown in the shipping plan. |
box_type |
string | "cubic" for custom cubic boxes; "flat_rate" for USPS flat-rate boxes. When "cubic" is used with a non-USPS carrier, the box is treated as a regular package (mode "package") and USPS cubic eligibility rules are not applied. |
outer_width |
number | Outer width in inches (decimals supported, e.g. 12.25). |
outer_length |
number | Outer length in inches (decimals supported). |
outer_depth |
number | Outer depth (height) in inches (decimals supported). |
inner_width |
number | Inner usable width in inches (decimals supported). |
inner_length |
number | Inner usable length in inches (decimals supported). |
inner_depth |
number | Inner usable depth in inches (decimals supported). |
empty_weight |
number | Empty box weight in ounces (decimals supported). Added to item weight when calculating shipment weight. |
max_weight |
number | Maximum payload weight in pounds (decimals supported). |
carrier_restriction |
string | Restrict this box to a specific carrier: "usps", "ups", "fedex", or "" (empty = available to all carriers). |
enabled |
boolean | Whether this box participates in packing/rating. Defaults to true when omitted (so legacy box definitions remain enabled). Set to false to temporarily disable a box (e.g. when stock is depleted) without removing it from the list. |
USPS Cubic Eligibility Rules (enforced automatically for USPS carriers only):
- Volume ≤ 0.5 cubic feet.
- Longest side ≤ 18 inches.
- Total package weight (items + box) ≤ 20 lbs (320 oz).
These rules are only enforced when the carrier is USPS (e.g. stamps_com, usps, endicia). Non-USPS carriers such as UPS and FedEx treat cubic-type boxes as regular packages — USPS cubic limits do not apply and the boxes are not excluded based on volume, longest side, or weight thresholds.
Boxes that do not meet these criteria when used with a USPS carrier are silently excluded from cubic candidates. They are still considered as flat-rate candidates if box_type is "flat_rate".
Example JSON:
[
{
"reference": "1 Bag",
"package_code": "package",
"package_name": "1 Bag",
"box_type": "cubic",
"outer_width": 8,
"outer_length": 6,
"outer_depth": 6,
"inner_width": 8,
"inner_length": 6,
"inner_depth": 6,
"empty_weight": 3,
"max_weight": 5,
"carrier_restriction": ""
},
{
"reference": "USPS Medium Flat Rate",
"package_code": "medium_flat_rate_box",
"package_name": "Medium Flat Rate Box",
"box_type": "flat_rate",
"outer_width": 14,
"outer_length": 12,
"outer_depth": 3,
"inner_width": 14,
"inner_length": 12,
"inner_depth": 3,
"empty_weight": 6,
"max_weight": 70,
"carrier_restriction": "usps"
}
]
Development
Running Tests
# Install all Composer dependencies (including dev)
composer install
# Run the full PHPUnit test suite
composer test
# Run a single test class
./vendor/bin/phpunit tests/Unit/ShipStationServiceTest.php
# Run a single test method
./vendor/bin/phpunit --filter test_build_test_package_plan_returns_best_rate
Tests live in tests/Unit/. A tests/bootstrap.php file provides WordPress and WooCommerce function stubs so the suite runs without a WordPress installation.
Test classes:
| File | Coverage |
|---|---|
tests/Unit/SettingsTest.php |
All getters, sanitization, defaults, get_boxes(), get_boxes_for_carrier(), sanitize_boxes_array(), multi-carrier settings, per-carrier service codes, ShipStation service pairs, box table UI rendering. |
tests/Unit/PackingServiceTest.php |
BoxPacker path, fallback path, pack_items(), unmeasured item handling, inner-dimensions packing, multi-package scenarios. |
tests/Unit/ShipEngineServiceTest.php |
Plan building, rate parsing, credential guards, build_all_test_package_plans(), per-carrier service code. |
tests/Unit/ShipStationServiceTest.php |
Plan building, Basic-Auth, sandbox logging, cheapest-rate selection, serviceCode/packageCode in requests, build_all_test_package_plans(), carrier/service code overrides. |
tests/Unit/PluginProcessOrderTest.php |
Multi-carrier order processing, cheapest-rate selection across carriers, single-carrier fallback. |
tests/Unit/TestPricingServiceTest.php |
Item expansion, multi-carrier routing, warning generation. |
tests/Unit/AdminTestUiTest.php |
Page rendering, nonce flow, form repopulation, results table, sandbox badge. |
tests/Unit/ShippingMethodTest.php |
Rate calculation, caching, cache key composition (carrier, boxes, display settings, service code), show-package-count labels. |
Building
Use bin/build.sh to create a production-ready ZIP that excludes all development files (listed in .distignore):
# Build with the version from the plugin header
bash bin/build.sh
# The ZIP is written to build/woocommerce-fk-usps-optimizer-<version>.zip
The script installs production-only Composer dependencies (--no-dev), copies plugin files via rsync, and then restores your local dev dependencies.
CI pipeline:
Every CI run produces a downloadable ZIP artifact:
- The CI workflow (
ci.yml) runs lint, PHPCS, and PHPUnit tests across PHP 8.0–8.3. After all matrix jobs pass, abuildjob runsbin/build.shand uploads the resulting ZIP as a GitHub Actions artifact namedplugin-zip(retained for 30 days). Download it from the Actions tab of any workflow run. - The Build workflow (
build.yml) triggers on every push tomain, runs the same build script, and commits the ZIP todist/so it is always available directly in the repository.
Code Style
The project follows the WordPress Coding Standards enforced via PHP_CodeSniffer:
# Check for violations
composer phpcs
# Auto-fix where possible
composer phpcbf
The PHPCS configuration lives in phpcs.xml.dist. The manage_woocommerce capability is registered as a custom known-good capability so PHPCS does not flag it.
Security note: the PHPCS toolchain (
squizlabs/php_codesniffer,wp-coding-standards/wpcs, and thephpcsstandards/*helpers) has had code-execution advisories in the past. Keep it patched:composer audit # Check for known advisories composer update squizlabs/php_codesniffer wp-coding-standards/wpcs \ --with-all-dependencies # Apply patches
Directory Structure
fk-usps-optimizer/
├── assets/
│ ├── css/
│ │ └── settings.css # Settings page styles (box table layout)
│ └── js/
│ └── settings.js # Carrier checkbox toggle + AJAX test connection + box table management
├── bin/
│ └── build.sh # Production build script
├── includes/
│ ├── class-admin-test-ui.php # Test Pricing admin page
│ ├── class-admin-ui.php # Order detail plan display
│ ├── class-boxpacker-box.php # dvdoug/boxpacker Box adapter
│ ├── class-boxpacker-item.php # dvdoug/boxpacker Item adapter
│ ├── class-order-plan-service.php
│ ├── class-packing-service.php
│ ├── class-pirateship-export.php
│ ├── class-plugin.php # Singleton bootstrap
│ ├── class-settings.php
│ ├── class-shipengine-service.php
│ ├── class-shipping-method.php # WooCommerce shipping method (zones integration)
│ ├── class-shipstation-service.php
│ └── class-test-pricing-service.php
├── tests/
│ ├── bootstrap.php # PHPUnit bootstrap with WP/WC stubs
│ └── Unit/
│ ├── AdminTestUiTest.php
│ ├── PackingServiceTest.php
│ ├── SettingsTest.php
│ ├── ShipEngineServiceTest.php
│ ├── ShipStationServiceTest.php
│ ├── ShippingMethodTest.php
│ └── TestPricingServiceTest.php
├── composer.json
├── composer.lock
├── phpcs.xml.dist
├── phpunit.xml.dist
├── readme.txt # WordPress.org plugin readme
├── README.md # This file
└── woocommerce-fk-usps-optimizer.php # Plugin entry point
Changelog
1.3.8
- Fixed: BoxPacker
NoBoxesAvailableExceptioncrash during checkout quantity changes. When a customer changes quantity in checkout, the AJAXupdate_order_reviewcall repacks items — if no box could fit all items, BoxPacker threw an uncaught exception resulting in a 500 error.items_fit_in_box()now catches the exception and returnsfalse, andpack_with_boxpacker()wrapspack()in a try/catch with fallback topack_fallback(). - Fixed: International shipment rate display for Canada and other non-US destinations. Carrier APIs require international service codes (e.g.
usps_priority_mail_international,ups_standard,fedex_international_ground) instead of domestic ones. Without this mapping, the APIs returned zero rates.resolve_service_code_for_destination()in bothShipEngine_ServiceandShipStation_Servicenow maps domestic codes to their international equivalents based on destination country, covering USPS, UPS, and FedEx. - Fixed:
usps_ground_advantageadded to the international service code map — mapping tousps_first_class_mail_internationalfor international destinations since Ground Advantage is domestic-only. Without this entry, additional ShipStation service-code pairs using Ground Advantage silently returned zero rates for Canada. - Fixed: Allow-list service code resolution for international checkout. When multiple ShipStation service pairs shared a carrier, the checkout path grouped them into a single API call with a domestic-code allow-list (e.g.
usps_priority_mail). International API responses returned international codes (e.g.usps_priority_mail_international), which didn't match the domestic allow-list — silently discarding all plans.resolve_code_for_country()now expands each allow-list entry to include both the original and international equivalent, so plans match regardless of which form the API returns. - New:
fk_usps_optimizer_intl_service_code_mapfilter — override the domestic-to-international service code mapping at runtime. Receives the default associative array and returns the modified map.
1.3.7
- Changed: Plugin rebranded to FunnelKit USPS Priority Shipping Optimizer for US & Canada — the plugin name, admin order metabox, Test Pricing page, settings page header, shipping method default title, and carrier API field descriptions now explicitly communicate support for both US and Canadian destinations. The plugin tag
canadahas been added toreadme.txt. - New: Comprehensive multi-agent AI development configuration —
CLAUDE.md(per-turn context),AGENTS.md(agent inventory + coordination rules),.context/(conventions, security checklist, testing guide), BMAD agents rewritten for this plugin, GitHub Custom Agents (developer, shipping reviewer, security reviewer), and.github/copilot-instructions.md.
1.3.6
- Fixed: BoxPacker heuristic could pick a suboptimal box size for a group of items even when a smaller box would fit. A post-processing optimisation (
Packing_Service::optimize_packed_boxes()) now checks each packed package and downsizes to the smallest fitting box — single-item packages use a straightforward dimensional match, while multi-item packages are re-packed through BoxPacker with only the candidate box. This prevents cases such as a Half-Off Magic Bag (5×5.5×2.5 in, 2 lb) being placed in a 2 Bag when it fits in a 1 Bag, or two Organic All-In-One grow bags (10.25×5.5×5 in each) being placed in a 4 Bag when they fit in a 3 Bag. - New:
Packing_Service::optimize_packed_boxes(),Packing_Service::items_fit_in_box(), andPacking_Service::rebind_package_to_box()methods that perform the post-packing downsizing pass.
1.3.5
- Changed: Send Packing Plan to PirateShip via Customer Note no longer writes the plan into the order's stored customer-note column. The plan is now persisted as private order meta (
_fk_packing_plan_note), rendered in the existing admin-only USPS Priority Shipping Plan metabox on the order edit screen, and injected into thecustomer_notefield of WooCommerce REST API responses (via thewoocommerce_rest_prepare_shop_order_objectfilter) so PirateShip continues to receive it. The previous hidden-marker (...) approach and thewoocommerce_order_get_customer_notestrip filter have been removed, eliminating the risk of the plan leaking through any admin path that bypasses the strip filter (custom metaboxes, third-party plugins, raw$context = 'edit'reads, direct DB queries). Orders processed by earlier versions are migrated lazily: on the next re-process the legacy marker block is removed from the persisted customer note and the plan is re-stored as meta. - New:
Plugin::PACKING_NOTE_META_KEYconstant identifying the order-meta key (_fk_packing_plan_note) that stores the rendered plan text. - New:
Plugin::inject_packing_plan_into_rest_response()filter callback hooked onwoocommerce_rest_prepare_shop_order_objectthat appends the stored plan to the REST response'scustomer_notefield when the setting is enabled. - Removed:
Plugin::filter_customer_note_for_display()and thewoocommerce_order_get_customer_notefilter registration — no longer needed because the plan never enters the customer-note column.
1.3.4
- New: PirateShip Notification Emails setting — comma-separated list of email addresses (newlines and semicolons are also accepted as separators) to notify after every order. The plugin sends each recipient a plain-text email containing the order's shipping address, the suggested packages with dimensions/weights/packing list, and a CSV file attachment that can be imported directly into [PirateShip](https://www
This README is longer than the copy stored here. Read the rest on GitHub →