WooCommerce Integration¶
Reference for App\Integrations\WooCommerce\WooCommerceIntegration. Covers the connectivity verification contract, the HTTP endpoint it targets, the three possible outcomes, and the test coverage for each.
Class¶
Namespace: App\Integrations\WooCommerce\WooCommerceIntegration
Extends: App\Services\AbstractIntegration
Connectivity Verification¶
WooCommerceIntegration implements performVerification(SystemConnection $connection): VerificationResult, which is called by the sealed AbstractIntegration::verify() method. Any uncaught Throwable from performVerification() is caught by verify() and converted into a failed VerificationResult, so callers never handle exceptions from the verification path.
What it does¶
- Reads
connection_tokenfrom the connection's encryptedcredentialsarray. - Strips any trailing
/wp-json/…path frombase_urlto derive$storeBase. - POSTs to
{storeBase}/wp-json/conflux-connector/v1/connectivity-checkwithconnection_tokenandconnection_idin the request body. - Returns a
VerificationResultbased on the HTTP response status.
Endpoint¶
Request body
| Field | Type | Description |
|---|---|---|
connection_token |
string | Token from credentials['connection_token']; empty string if absent |
connection_id |
string | UUID of the SystemConnection |
Outcomes¶
| HTTP status | VerificationResult returned |
|---|---|
200 |
VerificationResult::success($storeBase) — successful = true, externalUserId = $storeBase |
401 |
VerificationResult::failure('Invalid connection token.') — successful = false, message = 'Invalid connection token.' |
| Any other status | $response->throw() raises an Illuminate\Http\Client\RequestException, caught by AbstractIntegration::verify() and converted to VerificationResult::failure($e->getMessage()) |
VerificationResult shape¶
Namespace: App\Data\VerificationResult
| Property | Type | Description |
|---|---|---|
successful |
bool |
true on success, false on any failure |
externalUserId |
?string |
The store base URL on success; null on failure |
message |
?string |
Human-readable failure reason; null on success |
Bulk Export¶
WooCommerceIntegration implements fetchBulkExport(SystemConnection $connection, ExportRequest $request): Collection, called by the sealed AbstractIntegration::requestBulkExport(). Supported capabilities: SystemCapability::Customers, SystemCapability::Orders, and SystemCapability::Products. The endpointAndMapper() method is exhaustive — all three capabilities are handled and there is no fall-through branch.
Capability dispatch¶
The private endpointAndMapper() helper maps a capability to its WC REST endpoint and item mapper:
| Capability | Endpoint | Mapper |
|---|---|---|
SystemCapability::Customers |
/wp-json/wc/v3/customers |
WooCommerceTransformer::mapCustomer() |
SystemCapability::Orders |
/wp-json/wc/v3/orders |
WooCommerceTransformer::mapOrder() |
SystemCapability::Products |
/wp-json/wc/v3/products |
WooCommerceTransformer::mapProduct() |
Credentials¶
Bulk export authenticates via HTTP Basic Auth using WooCommerce REST API keys read from the connection's encrypted credentials array:
| Credential key | Description |
|---|---|
consumer_key |
WooCommerce REST API consumer key |
consumer_secret |
WooCommerce REST API consumer secret |
Delta sync (modified_after)¶
When ExportRequest::$modifiedAfter is set, the integration appends a modified_after query parameter (ISO 8601 string) to every request made by performProbeBulkExport and performFetchBulkExportPageRange. The WooCommerce REST API filters results server-side so that only records with a date_modified value later than the supplied timestamp are returned. When modifiedAfter is null, the parameter is omitted and all records are returned (full sync).
The cursor is supplied by BulkSyncJob — the integration never reads or writes it directly. See Delta Sync for the cursor derivation rules.
Pagination¶
All three endpoints share the same pagination contract. The integration passes page (integer, starts at 1) and per_page (50) as query parameters, reads the X-WP-TotalPages response header to determine the total number of pages, and iterates until all pages are fetched. If the header is absent or empty it defaults to 1 page.
Error handling¶
Any non-2xx response causes fetchBulkExportBatches()/performProbeBulkExport()/performFetchBulkExportPageRange() to throw IntegrationRequestException with the HTTP status code. AbstractIntegration's sealed wrapper methods pass this through unchanged; any other Throwable is wrapped in InvalidIntegrationResponseException.
Mapping: WooCommerce order → OrderData¶
WooCommerceTransformer::mapOrder() is a public static method (callable via first-class callable syntax as the mapper closure).
| WooCommerce field | OrderData property |
Notes |
|---|---|---|
id |
sourceId |
Cast to string |
| (constant) | sourceSystem |
Always SystemDriver::WooCommerce |
number |
sourceOrderNumber |
Human-readable order number; cast to string; null if absent |
status |
status |
Raw string — not normalised (e.g. completed, processing) |
currency |
currency |
ISO 4217 code |
total |
total |
Integer minor currency units (round(float × 100)); defaults to 0 |
total_tax |
totalTax |
Integer minor currency units; null if absent or non-numeric |
shipping_total |
totalShipping |
Integer minor currency units; null if absent or non-numeric |
date_created |
createdAt |
ISO 8601 string |
date_modified |
updatedAt |
ISO 8601 string |
date_paid |
datePaid |
ISO 8601 string; null if absent or empty string |
customer_id |
customerSourceId |
Cast to string; null if customer_id is 0 or absent (guest orders) |
billing.* |
addressBilling |
null if all billing fields are empty |
shipping.* |
addressShipping |
null if all shipping fields are empty |
line_items |
lineItems |
Collection<int, LineItemData> — see line item mapping below |
Unmapped scalar top-level fields (e.g. discount_total, payment_method) are captured as customAttributes. Fields in ORDER_MAPPED_KEYS and non-scalar values are excluded.
Mapping: WooCommerce line item → LineItemData¶
Each entry in the line_items array is mapped by the private mapLineItem() method and collected into OrderData::lineItems.
| WooCommerce field | LineItemData property |
Notes |
|---|---|---|
id |
sourceId |
Cast to string |
product_id |
productSourceId |
Cast to string — used by AbstractTransformer::storeOrder() to look up the previously-imported Product row by (system_connection_id, source_id) |
name |
name |
Product name at time of order |
quantity |
quantity |
Integer; defaults to 0 if absent |
price |
unitPrice |
Integer minor currency units; defaults to 0 if absent |
total |
total |
Integer minor currency units; defaults to 0 if absent |
sku |
sku |
null if absent or empty string |
Product linking
productSourceId carries the WooCommerce product_id. When AbstractTransformer::storeOrder() persists a line item it queries Product on (system_connection_id, product_source_id) and writes the resolved UUID to line_items.product_id. If the product has not been synced yet, product_id remains null until the order is re-imported after the Products sync (CFX-83) runs.
Mapping: WooCommerce customer → CustomerData¶
Field mapping lives on WooCommerceTransformer::mapCustomer() (public static), called directly by WooCommerceIntegration from each of the three fetch hooks above — it is not invoked through the TransformerContract::transform() interface.
| WooCommerce field | CustomerData property |
Notes |
|---|---|---|
id |
sourceId |
Cast to string |
| (constant) | sourceSystem |
Always SystemDriver::WooCommerce |
email |
email |
— |
date_created |
createdAt |
ISO 8601 string |
date_modified |
updatedAt |
ISO 8601 string |
first_name |
firstName |
null if empty |
last_name |
lastName |
null if empty |
billing.phone |
phone |
null if empty |
billing.* |
addressBilling |
null if all billing fields are empty |
shipping.* |
addressShipping |
null if all shipping fields are empty |
orders_count |
orderCount |
— |
total_spent |
totalSpent |
Converted to integer minor currency units (round(float × 100)) |
(remaining scalar top-level fields, plus meta_data entries) |
customAttributes |
Anything not in the mapped-keys list; see WooCommerceTransformer::MAPPED_KEYS |
Unmapped scalar top-level fields and meta_data entries with scalar values are captured as customAttributes. meta_data entries whose values are arrays or objects are silently skipped.
Mapping: WooCommerce product → ProductData¶
| WooCommerce field | ProductData property |
Notes |
|---|---|---|
id |
sourceId |
Cast to string |
| (constant) | sourceSystem |
Always SystemDriver::WooCommerce |
name |
name |
— |
status |
status |
See status mapping below |
date_created |
createdAt |
ISO 8601 string |
date_modified |
updatedAt |
ISO 8601 string |
sku |
sku |
null if absent or empty |
price |
price |
Integer minor currency units (round(float × 100)); null if absent or non-numeric |
regular_price |
originalPrice |
Integer minor currency units (round(float × 100)); null if absent or non-numeric |
categories[*].name |
categories |
Flat array of category name strings; empty objects and blank names skipped |
tags[*].name |
tags |
Flat array of tag name strings; empty objects and blank names skipped |
images[0].src |
imageUrl |
First image only; null if images is empty or src is blank |
permalink |
url |
null if absent or empty |
description |
description |
HTML stripped via strip_tags(); null if absent or empty |
Status mapping¶
WooCommerce status |
ProductStatus |
|---|---|
publish |
Active |
trash |
Deleted |
(any other: draft, pending, private, …) |
Inactive |
Unmapped scalar top-level fields (e.g. weight, stock_quantity, featured) are captured as customAttributes. Fields in PRODUCT_MAPPED_KEYS and non-scalar values are excluded.
PHPStan note¶
base_url is declared as string|null on SystemConnection. Before passing it to preg_replace(), the integration casts it explicitly: (string) $connection->base_url. This satisfies PHPStan level 10 (CFX-80).
Test coverage¶
Tests live in tests/Unit/Integrations/WooCommerce/WooCommerceIntegrationTest.php and tests/Unit/Integrations/WooCommerce/WooCommerceTransformerTest.php. All tests use Http::fake() to stub the HTTP layer — no database or network access.
performVerification tests
| Test | Stubbed response | Assertion |
|---|---|---|
returns success on 200 response |
HTTP 200 |
result->successful is true; result->externalUserId equals the store base URL |
returns failure with invalid token message on 401 |
HTTP 401 |
result->successful is false; result->message is 'Invalid connection token.' |
returns failure on network error |
HTTP 500 |
result->successful is false |
probeBulkExport tests
| Test | What it covers |
|---|---|
returns total pages from API header |
X-WP-TotalPages is read from a page-1 probe request |
throws IntegrationRequestException on HTTP error |
A 5xx response raises IntegrationRequestException |
fetchBulkExport tests (exercised via requestBulkExport(), which drives fetchBulkExportBatches())
| Test | What it covers |
|---|---|
maps WooCommerce customer fields to CustomerData |
Full field mapping including billing address, phone, orderCount, and totalSpent conversion |
fetches all pages when paginating |
X-WP-TotalPages drives multi-page iteration |
sets billing address to null when all billing fields are empty |
Empty address arrays map to null on both billing and shipping |
throws IntegrationRequestException on HTTP error |
A 5xx response raises IntegrationRequestException |
probeBulkExport — Products tests
| Test | What it covers |
|---|---|
returns total pages from API header |
X-WP-TotalPages header on GET /wp-json/wc/v3/products is read and returned as BulkExportProbe.totalPages |
throws IntegrationRequestException on HTTP error |
A 5xx response raises IntegrationRequestException |
fetchBulkExport — Products tests
| Test | What it covers |
|---|---|
maps WooCommerce product fields to ProductData |
Full field mapping: sourceId, status, sku, price/originalPrice in cents, imageUrl, categories, url |
fetches all pages when paginating |
X-WP-TotalPages drives multi-page iteration for the products endpoint |
throws IntegrationRequestException on HTTP error |
A 5xx response raises IntegrationRequestException |
probeBulkExport — Orders tests
| Test | What it covers |
|---|---|
returns total pages from API header for orders |
X-WP-TotalPages header on GET /wp-json/wc/v3/orders is read and returned as BulkExportProbe.totalPages |
fetchBulkExport — Orders tests
| Test | What it covers |
|---|---|
maps WooCommerce order fields to OrderData |
Full field mapping: sourceId, sourceOrderNumber, status, currency, total in cents, customerSourceId, and lineItems collection with productSourceId |
fetches all pages when paginating |
X-WP-TotalPages drives multi-page iteration for the orders endpoint |
throws IntegrationRequestException on HTTP error |
A 5xx response raises IntegrationRequestException |
WooCommerceTransformer::mapOrder tests
| Test | What it covers |
|---|---|
maps core fields correctly |
All primary order fields round-trip correctly, including monetary conversion to cents |
maps line items with productSourceId from WC product_id |
product_id → productSourceId; price → unitPrice in cents; sku, name, quantity |
prices are converted to cents |
total, total_tax, shipping_total converted via round(float × 100) |
customerSourceId is null when customer_id is 0 |
Guest orders (customer_id: 0) produce null |
datePaid is null when absent |
Empty date_paid maps to null |
optional monetary fields default to null when absent |
Absent total_tax, shipping_total, date_paid, customerSourceId, addresses, and line_items produce correct nulls / empty collection |
line item sku is null when empty string |
Empty sku on a line item maps to null |
captures unmapped scalar fields as customAttributes |
discount_total, payment_method, etc. appear in customAttributes |
does not capture mapped keys as customAttributes |
id, status, total, line_items, billing are excluded |
WooCommerceTransformer::mapProduct tests
| Test | What it covers |
|---|---|
maps core fields correctly |
All primary fields round-trip correctly |
status mapping (5 sub-tests) |
publish → Active; trash → Deleted; draft, pending, private → Inactive |
price is converted to cents |
"9.99" → 999, "14.99" → 1499 |
price is null when empty string |
Empty price / regular_price map to null |
categories and tags are extracted as name arrays |
Objects with name keys flatten to string arrays |
imageUrl comes from first image src |
Only images[0].src is used |
imageUrl is null when images array is empty |
Missing images produce null |
description strips HTML tags |
strip_tags() is applied |
optional fields default to null when absent |
Absent sku, price, originalPrice, imageUrl, url, description are null; categories and tags are empty arrays |
captures unmapped scalar fields as customAttributes |
weight, stock_quantity, featured appear in customAttributes |
does not capture mapped keys as customAttributes |
id, name, status, sku, price, categories, images are excluded |
Source files¶
| File | Purpose |
|---|---|
app/Integrations/WooCommerce/WooCommerceIntegration.php |
Integration implementation — performVerification, fetchBulkExportBatches, performProbeBulkExport, performFetchBulkExportPageRange, endpointAndMapper |
app/Integrations/WooCommerce/WooCommerceTransformer.php |
Field mapping — mapCustomer(), mapOrder(), and mapProduct() public static methods; mapLineItem() and mapOrderCustomAttributes() private static helpers |
tests/Unit/Integrations/WooCommerce/WooCommerceIntegrationTest.php |
Unit tests for verification and all three bulk-export capabilities |
tests/Unit/Integrations/WooCommerce/WooCommerceTransformerTest.php |
Unit tests for mapOrder(), mapProduct(), and mapCustomer() |
See also¶
- Integration Contract reference —
AbstractIntegration,VerificationResult,IntegrationContract, and all shared DTOs - Integration Contract design — reasoning behind the verification and error-handling design
- Implement a System Integration — step-by-step guide for adding a new integration
- Transformer Contract reference —
AbstractTransformer,TransformerRegistry, and the unified Eloquent models - Transformer Pipeline design — reasoning behind the transformation and storage design
- PrestaShop Integration reference — the other registered integration, including its own customer bulk-export implementation