diff --git a/woo-business-central/admin/class-wbc-admin.php b/woo-business-central/admin/class-wbc-admin.php
index 7ff33b8..a12097d 100644
--- a/woo-business-central/admin/class-wbc-admin.php
+++ b/woo-business-central/admin/class-wbc-admin.php
@@ -86,6 +86,9 @@ class WBC_Admin {
register_setting( 'wbc_sync', 'wbc_enable_brand_sync', array(
'sanitize_callback' => array( $this, 'sanitize_checkbox' ),
) );
+ register_setting( 'wbc_sync', 'wbc_category_parent_map', array(
+ 'sanitize_callback' => 'sanitize_textarea_field',
+ ) );
// Order settings (own group)
register_setting( 'wbc_orders', 'wbc_enable_order_sync', array(
diff --git a/woo-business-central/admin/partials/wbc-admin-display.php b/woo-business-central/admin/partials/wbc-admin-display.php
index 64fe84c..27b87d5 100644
--- a/woo-business-central/admin/partials/wbc-admin-display.php
+++ b/woo-business-central/admin/partials/wbc-admin-display.php
@@ -233,10 +233,23 @@ $tabs = array(
-
+
+
+
+
+
+ |
+
+ |
+
+
+
+
|
diff --git a/woo-business-central/includes/class-wbc-batch-sync.php b/woo-business-central/includes/class-wbc-batch-sync.php
index a954576..b576f7c 100644
--- a/woo-business-central/includes/class-wbc-batch-sync.php
+++ b/woo-business-central/includes/class-wbc-batch-sync.php
@@ -29,8 +29,16 @@ class WBC_Batch_Sync {
/**
* Number of BC items fetched per page in category-filtered mode
+ *
+ * Kept small on purpose: each item can trigger several sequential BC
+ * calls while processing it (location stock, price lists, brand/category
+ * lookup), not just the one page fetch. 100 items/page was measured to
+ * take 150-250+ seconds per page - long enough to exceed PHP's
+ * execution limit and strand the action mid-run ("in-progress" forever,
+ * never completing or erroring). Matches BATCH_SIZE, which is proven to
+ * finish reliably within one execution.
*/
- const CATEGORY_PAGE_SIZE = 100;
+ const CATEGORY_PAGE_SIZE = 25;
/**
* Option name storing the current/last run's progress
diff --git a/woo-business-central/includes/class-wbc-product-sync.php b/woo-business-central/includes/class-wbc-product-sync.php
index f585114..ee1886b 100644
--- a/woo-business-central/includes/class-wbc-product-sync.php
+++ b/woo-business-central/includes/class-wbc-product-sync.php
@@ -630,12 +630,14 @@ class WBC_Product_Sync {
}
/**
- * Sync WooCommerce product_brand (leaf BC item category) and product_cat
- * (parent BC item category) from BC.
+ * Sync WooCommerce product_brand (BC item category) from BC, and its
+ * WooCommerce parent product category from a manually configured map.
*
- * Requires the custom ItemCategoryHierarchy OData endpoint (exposes
- * Code, Description, Parent_Category) since the standard BC API's
- * itemCategories entity has no parent-category field.
+ * BC's item categories here are flat (e.g. "DAVINES", "CANTABRIA LABS")
+ * with no parent-category field on the Item Category table to walk up
+ * from brand to a grouping like "Haircare" - confirmed against the
+ * published CategProd endpoint, which only exposes Code and Description.
+ * That grouping is maintained in wp-admin instead (wbc_category_parent_map).
*
* @param WC_Product $product WooCommerce product.
* @param array $item BC item data.
@@ -649,26 +651,19 @@ class WBC_Product_Sync {
}
$category_info = $this->get_category_info( $category_code );
-
- if ( ! $category_info ) {
- return false;
- }
+ $brand_name = ( $category_info ? $category_info['description'] : '' ) ?: $category_code;
$changed = false;
- // Leaf category -> brand (e.g. "Davines")
- $brand_name = $category_info['description'] ?: $category_code;
if ( taxonomy_exists( 'product_brand' ) ) {
if ( $this->set_product_taxonomy_term( $product->get_id(), 'product_brand', $brand_name ) ) {
$changed = true;
}
}
- // Parent category -> WooCommerce product category (e.g. "Haircare")
- if ( ! empty( $category_info['parent_code'] ) ) {
- $parent_info = $this->get_category_info( $category_info['parent_code'] );
- $parent_name = $parent_info ? ( $parent_info['description'] ?: $category_info['parent_code'] ) : $category_info['parent_code'];
+ $parent_name = $this->get_mapped_parent_category( $category_code );
+ if ( ! empty( $parent_name ) ) {
if ( $this->set_product_taxonomy_term( $product->get_id(), 'product_cat', $parent_name ) ) {
$changed = true;
}
@@ -678,27 +673,31 @@ class WBC_Product_Sync {
}
/**
- * Get category info (description, parent code) from the custom
- * ItemCategoryHierarchy OData endpoint, cached per sync run.
+ * Get category info (description) from the custom CategProd OData
+ * endpoint, cached per sync run.
+ *
+ * Uses the standard v2.0 itemCategories entity (id, code, displayName) -
+ * no custom endpoint needed for this part, since the display name is
+ * already exposed by the standard API. Only the WooCommerce parent
+ * category grouping needs the manual map (see get_mapped_parent_category()),
+ * because that's the part with no BC equivalent at all.
*
* @param string $category_code BC item category code.
- * @return array|false Array with 'description' and 'parent_code', or false.
+ * @return array|false Array with 'description', or false.
*/
private function get_category_info( $category_code ) {
if ( isset( $this->category_cache[ $category_code ] ) ) {
return $this->category_cache[ $category_code ];
}
- $escaped = WBC_API_Client::escape_odata_string( $category_code );
-
- $result = WBC_API_Client::odata_get( 'ItemCategoryHierarchy', array(
- '$filter' => "Code eq '" . $escaped . "'",
- '$select' => 'Code,Description,Parent_Category',
+ $result = WBC_API_Client::get( '/itemCategories', array(
+ '$filter' => "code eq '" . WBC_API_Client::escape_odata_string( $category_code ) . "'",
+ '$select' => 'code,displayName',
'$top' => 1,
) );
if ( is_wp_error( $result ) ) {
- WBC_Logger::warning( 'ProductSync', 'Failed to query ItemCategoryHierarchy', array(
+ WBC_Logger::warning( 'ProductSync', 'Failed to query itemCategories', array(
'category_code' => $category_code,
'error' => $result->get_error_message(),
) );
@@ -714,8 +713,7 @@ class WBC_Product_Sync {
}
$info = array(
- 'description' => $entries[0]['Description'] ?? '',
- 'parent_code' => $entries[0]['Parent_Category'] ?? '',
+ 'description' => $entries[0]['displayName'] ?? '',
);
$this->category_cache[ $category_code ] = $info;
@@ -723,6 +721,38 @@ class WBC_Product_Sync {
return $info;
}
+ /**
+ * Look up the WooCommerce parent product category for a BC category code
+ * from the manually configured map (wbc_category_parent_map option:
+ * one "CODE=Parent Name" pair per line).
+ *
+ * @param string $category_code BC item category code.
+ * @return string Parent category name, or '' if not mapped.
+ */
+ private function get_mapped_parent_category( $category_code ) {
+ $raw = get_option( 'wbc_category_parent_map', '' );
+
+ if ( empty( trim( $raw ) ) ) {
+ return '';
+ }
+
+ foreach ( preg_split( '/\r\n|\r|\n/', $raw ) as $line ) {
+ $line = trim( $line );
+
+ if ( $line === '' || strpos( $line, '=' ) === false ) {
+ continue;
+ }
+
+ list( $code, $parent_name ) = array_map( 'trim', explode( '=', $line, 2 ) );
+
+ if ( strcasecmp( $code, $category_code ) === 0 ) {
+ return $parent_name;
+ }
+ }
+
+ return '';
+ }
+
/**
* Assign a product to a single term in a taxonomy, creating the term if needed.
*