WP Manifestindependent plugin directory
manifest / ecommerce / nv-boxpacker

FunnelKit USPS Priority Shipping Optimizer for US & Canada

NV Digital BoxPacker - ShipEnine | Shipstation API - USPS Priority Shipping Optimizer

★ 0stars

Install

Download source zip No release zip yet

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.zip
No built-in updater. New versions won't appear under Dashboard → Updates. Install new releases yourself, or let Git Updater track the repository.

USPS 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

  1. Overview
  2. Requirements
  3. Installation
  4. Configuration
  5. Features
  6. Architecture
  7. WordPress Hooks and Filters
  8. Box Definition JSON Schema
  9. Development
  10. Changelog

Overview

When a customer completes checkout, this plugin:

  1. Collects all shippable items from the WooCommerce order.
  2. Packs them using the dvdoug/boxpacker library (falls back to a single-item-per-box strategy when the library is unavailable).
  3. 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").
  4. Stores a complete, package-level shipping plan on the order for admin review.
  5. 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

  1. Download or build the plugin zip (see bin/build.sh).
  2. Upload and activate through Plugins → Add New → Upload Plugin in WordPress, or extract to wp-content/plugins/fk-usps-optimizer/.
  3. Open WooCommerce → USPS Optimizer to complete configuration.

Note: vendor/ is excluded from the repository. Run composer install --no-dev inside 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_url filter (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:

  1. The configured ShipEngine API Key authenticates successfully against the ShipEngine API.
  2. The configured Carrier ID belongs to an active USPS carrier account (stamps_com, usps, or endicia).

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:

  1. Cart items are extracted and converted to the item format expected by Packing_Service::pack_items().
  2. 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.
  3. 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.
  4. 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:

  1. Collects shippable items via Packing_Service::pack_order().
  2. Packs them using BoxPacker (or fallback).
  3. Rate-shops every packed package across all enabled carrier services and picks the cheapest rate.
  4. Saves the result to the order meta key _fk_usps_shipping_plan as a serialized PHP array.
  5. 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:

  1. Enter one or more items with name, quantity, dimensions (inches), and weight (ounces).
  2. Enter the destination address.
  3. 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, a build job runs bin/build.sh and uploads the resulting ZIP as a GitHub Actions artifact named plugin-zip (retained for 30 days). Download it from the Actions tab of any workflow run.
  • The Build workflow (build.yml) triggers on every push to main, runs the same build script, and commits the ZIP to dist/ 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 the phpcsstandards/* 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 NoBoxesAvailableException crash during checkout quantity changes. When a customer changes quantity in checkout, the AJAX update_order_review call 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 returns false, and pack_with_boxpacker() wraps pack() in a try/catch with fallback to pack_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 both ShipEngine_Service and ShipStation_Service now maps domestic codes to their international equivalents based on destination country, covering USPS, UPS, and FedEx.
  • Fixed: usps_ground_advantage added to the international service code map — mapping to usps_first_class_mail_international for 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_map filter — 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 canada has been added to readme.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(), and Packing_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 the customer_note field of WooCommerce REST API responses (via the woocommerce_rest_prepare_shop_order_object filter) so PirateShip continues to receive it. The previous hidden-marker (...) approach and the woocommerce_order_get_customer_note strip 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_KEY constant 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 on woocommerce_rest_prepare_shop_order_object that appends the stored plan to the REST response's customer_note field when the setting is enabled.
  • Removed: Plugin::filter_customer_note_for_display() and the woocommerce_order_get_customer_note filter 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 →