fix: correct category-page size and brand/category source of truth
Two fixes bundled together: 1. Shrink CATEGORY_PAGE_SIZE from 100 to 25. Each item in a category page can trigger several sequential BC calls (location stock, price lists, brand/category lookup) beyond the page fetch itself - at 100 items/page this measured 150-250+ seconds on live data, exceeding PHP's execution limit and leaving the Action Scheduler action stuck "in-progress" forever instead of completing or failing visibly. 2. BC's Item Category table has no parent-category field - confirmed via the actual published CategProd endpoint (Code, Description only) and the standard itemCategories entity (id, code, displayName only). The original ItemCategoryHierarchy AL snippet assumed a Parent Category field that doesn't exist. Switched to the standard /itemCategories API for the brand display name (no custom endpoint needed for that part), and replaced the BC-side parent-category walk with a WordPress-side manual map (wbc_category_parent_map: "CODE=Parent Name" per line) for the WooCommerce product category grouping, since BC has nothing to offer there. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -86,6 +86,9 @@ class WBC_Admin {
|
|||||||
register_setting( 'wbc_sync', 'wbc_enable_brand_sync', array(
|
register_setting( 'wbc_sync', 'wbc_enable_brand_sync', array(
|
||||||
'sanitize_callback' => array( $this, 'sanitize_checkbox' ),
|
'sanitize_callback' => array( $this, 'sanitize_checkbox' ),
|
||||||
) );
|
) );
|
||||||
|
register_setting( 'wbc_sync', 'wbc_category_parent_map', array(
|
||||||
|
'sanitize_callback' => 'sanitize_textarea_field',
|
||||||
|
) );
|
||||||
|
|
||||||
// Order settings (own group)
|
// Order settings (own group)
|
||||||
register_setting( 'wbc_orders', 'wbc_enable_order_sync', array(
|
register_setting( 'wbc_orders', 'wbc_enable_order_sync', array(
|
||||||
|
|||||||
@@ -233,10 +233,23 @@ $tabs = array(
|
|||||||
<label>
|
<label>
|
||||||
<input type="checkbox" name="wbc_enable_brand_sync" value="yes"
|
<input type="checkbox" name="wbc_enable_brand_sync" value="yes"
|
||||||
<?php checked( get_option( 'wbc_enable_brand_sync', 'no' ), 'yes' ); ?> />
|
<?php checked( get_option( 'wbc_enable_brand_sync', 'no' ), 'yes' ); ?> />
|
||||||
<?php esc_html_e( 'Sync BC item category to the product_brand taxonomy, and its parent category to the WooCommerce product category', 'woo-business-central' ); ?>
|
<?php esc_html_e( 'Sync BC item category to the product_brand taxonomy', 'woo-business-central' ); ?>
|
||||||
</label>
|
</label>
|
||||||
<p class="description">
|
<p class="description">
|
||||||
<?php esc_html_e( 'Requires the custom ItemCategoryHierarchy OData endpoint and the WooCommerce Brands plugin (product_brand taxonomy).', 'woo-business-central' ); ?>
|
<?php esc_html_e( 'Uses the standard itemCategories API. Requires the WooCommerce Brands plugin (product_brand taxonomy).', 'woo-business-central' ); ?>
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="wbc_category_parent_map"><?php esc_html_e( 'Category Parent Map', 'woo-business-central' ); ?></label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<textarea id="wbc_category_parent_map" name="wbc_category_parent_map"
|
||||||
|
class="large-text code" rows="6"
|
||||||
|
placeholder="DAVINES=Haircare OBAGI=Skincare"><?php echo esc_textarea( get_option( 'wbc_category_parent_map', '' ) ); ?></textarea>
|
||||||
|
<p class="description">
|
||||||
|
<?php esc_html_e( 'Optional. One "CODE=Parent Category Name" pair per line. Business Central\'s item categories here are flat (no parent-category field), so the WooCommerce product category grouping (e.g. brand "Davines" under category "Haircare") is maintained here rather than pulled from BC. Leave empty to skip - only the brand taxonomy will be set.', 'woo-business-central' ); ?>
|
||||||
</p>
|
</p>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -29,8 +29,16 @@ class WBC_Batch_Sync {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Number of BC items fetched per page in category-filtered mode
|
* 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
|
* Option name storing the current/last run's progress
|
||||||
|
|||||||
@@ -630,12 +630,14 @@ class WBC_Product_Sync {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sync WooCommerce product_brand (leaf BC item category) and product_cat
|
* Sync WooCommerce product_brand (BC item category) from BC, and its
|
||||||
* (parent BC item category) from BC.
|
* WooCommerce parent product category from a manually configured map.
|
||||||
*
|
*
|
||||||
* Requires the custom ItemCategoryHierarchy OData endpoint (exposes
|
* BC's item categories here are flat (e.g. "DAVINES", "CANTABRIA LABS")
|
||||||
* Code, Description, Parent_Category) since the standard BC API's
|
* with no parent-category field on the Item Category table to walk up
|
||||||
* itemCategories entity has no parent-category field.
|
* 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 WC_Product $product WooCommerce product.
|
||||||
* @param array $item BC item data.
|
* @param array $item BC item data.
|
||||||
@@ -649,26 +651,19 @@ class WBC_Product_Sync {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$category_info = $this->get_category_info( $category_code );
|
$category_info = $this->get_category_info( $category_code );
|
||||||
|
$brand_name = ( $category_info ? $category_info['description'] : '' ) ?: $category_code;
|
||||||
if ( ! $category_info ) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
$changed = false;
|
$changed = false;
|
||||||
|
|
||||||
// Leaf category -> brand (e.g. "Davines")
|
|
||||||
$brand_name = $category_info['description'] ?: $category_code;
|
|
||||||
if ( taxonomy_exists( 'product_brand' ) ) {
|
if ( taxonomy_exists( 'product_brand' ) ) {
|
||||||
if ( $this->set_product_taxonomy_term( $product->get_id(), 'product_brand', $brand_name ) ) {
|
if ( $this->set_product_taxonomy_term( $product->get_id(), 'product_brand', $brand_name ) ) {
|
||||||
$changed = true;
|
$changed = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parent category -> WooCommerce product category (e.g. "Haircare")
|
$parent_name = $this->get_mapped_parent_category( $category_code );
|
||||||
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'];
|
|
||||||
|
|
||||||
|
if ( ! empty( $parent_name ) ) {
|
||||||
if ( $this->set_product_taxonomy_term( $product->get_id(), 'product_cat', $parent_name ) ) {
|
if ( $this->set_product_taxonomy_term( $product->get_id(), 'product_cat', $parent_name ) ) {
|
||||||
$changed = true;
|
$changed = true;
|
||||||
}
|
}
|
||||||
@@ -678,27 +673,31 @@ class WBC_Product_Sync {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get category info (description, parent code) from the custom
|
* Get category info (description) from the custom CategProd OData
|
||||||
* ItemCategoryHierarchy OData endpoint, cached per sync run.
|
* 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.
|
* @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 ) {
|
private function get_category_info( $category_code ) {
|
||||||
if ( isset( $this->category_cache[ $category_code ] ) ) {
|
if ( isset( $this->category_cache[ $category_code ] ) ) {
|
||||||
return $this->category_cache[ $category_code ];
|
return $this->category_cache[ $category_code ];
|
||||||
}
|
}
|
||||||
|
|
||||||
$escaped = WBC_API_Client::escape_odata_string( $category_code );
|
$result = WBC_API_Client::get( '/itemCategories', array(
|
||||||
|
'$filter' => "code eq '" . WBC_API_Client::escape_odata_string( $category_code ) . "'",
|
||||||
$result = WBC_API_Client::odata_get( 'ItemCategoryHierarchy', array(
|
'$select' => 'code,displayName',
|
||||||
'$filter' => "Code eq '" . $escaped . "'",
|
|
||||||
'$select' => 'Code,Description,Parent_Category',
|
|
||||||
'$top' => 1,
|
'$top' => 1,
|
||||||
) );
|
) );
|
||||||
|
|
||||||
if ( is_wp_error( $result ) ) {
|
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,
|
'category_code' => $category_code,
|
||||||
'error' => $result->get_error_message(),
|
'error' => $result->get_error_message(),
|
||||||
) );
|
) );
|
||||||
@@ -714,8 +713,7 @@ class WBC_Product_Sync {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$info = array(
|
$info = array(
|
||||||
'description' => $entries[0]['Description'] ?? '',
|
'description' => $entries[0]['displayName'] ?? '',
|
||||||
'parent_code' => $entries[0]['Parent_Category'] ?? '',
|
|
||||||
);
|
);
|
||||||
|
|
||||||
$this->category_cache[ $category_code ] = $info;
|
$this->category_cache[ $category_code ] = $info;
|
||||||
@@ -723,6 +721,38 @@ class WBC_Product_Sync {
|
|||||||
return $info;
|
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.
|
* Assign a product to a single term in a taxonomy, creating the term if needed.
|
||||||
*
|
*
|
||||||
|
|||||||
Reference in New Issue
Block a user