# Transdirect Shipping for WooCommerce

The official Transdirect WooCommerce plugin: pulls live shipping quotes from
`transdirect.com.au`, renders them at cart and checkout, and syncs placed
orders back to the merchant's Transdirect account so they can be booked into
a carrier.

This README is for **developers working on the plugin**. For merchant-facing
documentation see `readme.txt` (the WordPress.org listing) and
`docs/CHANGELOG-9.0.0.md`.

---

## Architecture: what's plugin-side vs API-side

The Transdirect API does most of the heavy lifting. The plugin's job is to
be a thin storefront integration. Mixing them up has historically been the
source of bugs and dead code (v9.0.1 ripped out two duplicate logic paths).

| Concern | Where it lives |
|---|---|
| Quote display filter (`display_all` / `display_cheapest` / `display_cheapest_fastest`) | **API** — `ModuleDetails::filterQuotes` runs server-side; the plugin renders what it gets back. |
| Surcharge, fixed price on error, SKU/postcode rules | **API** — `ModuleDetails::applyTriggerAction` via `ModuleRules`. |
| Tail-lift pickup/delivery | **API** — `ModuleDetails::applyTailGate`. |
| Default item size fallback (when product has no dims) | **API** — `ModuleDetails::checkItems` substitutes account defaults before quoting. |
| Order Boxing (multi-item bin packing) | **API** — `app\utilities\groupitems\v2\Package3DBin`. |
| Default `street_type` (residential/commercial) | **API** — set when the customer hasn't actively chosen. |
| `shipping_title` carrier-label override | **Plugin** — backend stores it but doesn't apply it; rendered as the rate label when the merchant set one and a single quote was returned. |
| `mode` (`full_mode` / `simplified_mode` / `no_display_mode`) | **Plugin** — gates the storefront UI surface (cart calculator widget, checkout rates). |
| `qty_in_pkg` (Boxes per unit) | **Plugin** — cart-line concept the API doesn't see; multiplied once in `Quotes::td_get_cart_items`. |
| Suburb / postcode typeahead, force-selection | **Plugin** — pure checkout UX. |
| Residential / Commercial selector + persistence to order | **Plugin** — customer choice has to be captured on the storefront and stamped on the order so booking sync sends the matching `address_type`. |
| Order sync (`/orders/syncOrders` polling cron + retry backoff) | **Plugin** — WP cron in `OrderSync.php`. |

**Rule of thumb:** if a setting is configured on the Transdirect portal and
affects the *content* of a quote, the API has already applied it. If it
affects how the storefront *renders* or *collects* data, the plugin does it.

---

## Directory layout

```
admin/      Settings page, plugin-list link, migration notice
assets/     CSS / JS / bundled locality JSON
  data/locations.json          ~16k AU localities, refreshed daily
  js/td-suburb-typeahead.js    Classic + Block Checkout typeahead
  js/td-cart-widget.js         Standalone cart-page widget
frontend/   Public-side enqueues + scripts
includes/   Core PHP classes (PSR-4 under TransdirectShipping\Includes)
  SettingsPage.php             Global settings + v8 migration
  Quotes.php                   Single qty_in_pkg multiplication point
  WCTransdirectShipping.php    WC_Shipping_Method implementation
  OrderSync.php                Cron-driven order push with backoff
  AdditionalCheckoutField.php  Block Checkout "Delivery type" field
  SuburbLookup.php             REST endpoint + daily locality refresh
  Migration.php                v8 → v9 lift of zone settings to global
tests/
  Includes/*Test.php           PHPUnit (Brain Monkey for WP mocking)
  e2e/                         Playwright + WC QIT test package
docs/CHANGELOG-9.0.0.md        Merchant-facing release notes
```

---

## Local development

### Prerequisites

- PHP 7.4+ (the plugin must run on 7.4 → 8.x)
- Composer
- Node 20+ (only for the e2e suite)
- A local WordPress + WooCommerce site (Local by Flywheel works fine)
- A Transdirect API key on a sandbox or test account

### Setup

```bash
composer install
cd tests/e2e && npm install     # only if you'll run Playwright
```

Symlink or copy the repo into your local WP install's `wp-content/plugins/`
under the slug `transdirect-shipping` (see "Build / release" below for why
the folder name matters).

### Common commands

```bash
composer lint              # phpcs against WordPress + WooCommerce sniffs
composer lint:fix          # phpcbf
composer phpstan           # phpstan, memory bumped to 2G
composer phpunit           # tests/Includes/*Test.php
composer compat            # PHPCompatibilityWP against PHP 7.4
composer test              # all four above, sequenced
composer qit -- env:up     # boot a WC QIT environment (Docker)
composer qit -- env:down   # tear it down
```

---

## Tests

| Suite | Path | Runs |
|---|---|---|
| PHPUnit | `tests/Includes/` | Logic that can be tested in isolation with WP functions mocked via Brain Monkey. Boot config: `phpunit.xml.dist`. |
| Playwright E2E | `tests/e2e/` | Real WC QIT environment, real WP/WC, real Transdirect API. Bootstrapped by `tests/e2e/bin/setup.sh`. |
| WC QIT | `qit.json` | WooCommerce's hosted test pipeline. Environments matrix (default / min / older_woo / blocks); groups (`pr` / `release`). |
| Lint / static | `phpcs.xml.dist`, `phpstan.neon.dist` | WPCS + WooCommerce sniffs, PHPStan level configured in the neon. |

E2E + QIT need two repo secrets: `QIT_USER` / `QIT_ACCESS_TOKEN`, plus
`TD_API_KEY` for the Playwright job.

---

## Build / release

### Why the zip's folder name matters

WordPress and QIT identify a plugin by the **folder name inside its zip**,
which must match the plugin slug (`transdirect-shipping`) and the `Text
Domain` in the plugin header. The GitHub Actions checkout dir is named
`transdirect-wordpress-plugin` (the repo name), which does **not** match.

If you zip the checkout dir as-is, QIT's `env:up` fails to locate the plugin
under its slug and the rest of the pipeline is skipped silently. Both e2e
jobs work around this by rsyncing into a temp staging dir:

```bash
STAGING=$(mktemp -d)
rsync -a --exclude='.git' --exclude='.github' --exclude='node_modules' \
         --exclude='tests' --exclude='dist' ./ "$STAGING/transdirect-shipping/"
(cd "$STAGING" && zip -qr "$GITHUB_WORKSPACE/dist/transdirect-shipping.zip" transdirect-shipping)
```

If you're packaging a zip by hand, do the same — never `zip -r` the
working tree directly.

### Releasing to WordPress.org

1. Bump `Version:` in `transdirect-shipping.php`, `Stable tag:` in
   `readme.txt`, and add a changelog entry to both `readme.txt` and
   `docs/CHANGELOG-X.Y.Z.md`.
2. Open a PR, get it through CI, merge to `master`.
3. Push a tag matching `*` (e.g. `9.0.1`). The **Deploy to WordPress.org**
   workflow picks up any pushed tag and publishes the build.

---

## CI workflows

| File | Trigger | What it does |
|---|---|---|
| `.github/workflows/ci.yml` | PR + push | Lint, phpstan, phpunit, php-compat. Fast feedback gate. |
| `.github/workflows/e2e.yml` | PR + push | Builds the slug-named zip; runs WC QIT (`qit-managed` job) and standalone Playwright (`local-e2e` job) against a real Transdirect API. |
| `.github/workflows/deploy.yml` | Pushed tag | Builds the release zip and pushes to WordPress.org via SVN. |
| `.github/workflows/claude.yml` | `@claude` mention in PR/issue/review | Invokes the Claude Code GitHub Action (Sonnet 4.6) for in-place code review and small fixes. |

---

## Backend (Transdirect API) reference

The PHP backend lives in a separate repo (`transdirect`). Files worth
knowing when working on this plugin:

- `app/libraries/api/controllers/BookingsController.php`
  - `apiv2create()` — the main quote endpoint the plugin POSTs to
  - `getApiSettings()` — what `/api_details` returns (what the plugin can see)
- `app/models/ModuleDetails.php`
  - `filterQuotes()`, `applyTriggerAction()`, `applyTailGate()`,
    `applyDefaultSettings()`, `checkItems()` — the server-side appliers
    listed in the architecture table above
- `app/utilities/groupitems/v2/Package3DBin.php` — Order Boxing

If a piece of merchant-configured behaviour isn't reflected in a quote, the
fix is almost always on that side, not here.

---

## Where work is tracked

- Linear: project **E-commerce Integrations — WooCommerce & Shopify**, tag
  `WordPress`. Branches use `josholiver/tra-<n>-<slug>`.
- Customer-facing escalations come through Freshdesk; Linear tickets that
  originate there reference `#<ticket-id>`.
