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. *