PluginProbe ʕ •ᴥ•ʔ
CommerceBird – AI Command Center, ERP Integrations & B2B for WooCommerce (Zoho, Exact Online). / 2.7.3
CommerceBird – AI Command Center, ERP Integrations & B2B for WooCommerce (Zoho, Exact Online). v2.7.3
3.0.3 3.0.2 3.0.1 trunk 2.2.14 2.2.15 2.2.16 2.2.17 2.2.18 2.2.19 2.3.0 2.3.1 2.3.10 2.3.11 2.3.12 2.3.13 2.3.14 2.3.2 2.3.3 2.3.4 2.3.5 2.3.6 2.3.7 2.3.8 2.3.9 2.4.0 2.4.1 2.4.2 2.4.3 2.4.4 2.4.5 2.4.6 2.5.0 2.5.1 2.5.2 2.6.0 2.6.1 2.6.2 2.6.3 2.6.4 2.6.5 2.7.0 2.7.1 2.7.2 2.7.3 2.7.4 2.7.5 2.7.6 2.7.7 2.7.8 2.7.9 2.7.91 2.7.92 2.7.93 2.8.0 2.8.1 2.8.2 2.8.3 2.8.4 2.8.5 2.9.0 2.9.1 2.9.2 2.9.3 3.0.0
commercebird / includes / classes / zoho-inventory / class-import-items.php
commercebird / includes / classes / zoho-inventory Last commit date
class-cmbird-categories-zi.php 7 months ago class-cmbird-image-zi.php 5 months ago class-import-items.php 5 months ago class-import-price-list.php 9 months ago class-multi-currency.php 7 months ago class-order-sync.php 5 months ago class-product.php 5 months ago class-users-contact.php 5 months ago index.php 1 year ago
class-import-items.php
1864 lines
1 <?php
2
3 /**
4 * Class to import Products from Zoho to WooCommerce
5 *
6 * @package zoho_inventory_api
7 */
8 if ( ! defined( 'ABSPATH' ) ) {
9 exit;
10 }
11
12 use Automattic\WooCommerce\Internal\ProductAttributesLookup\LookupDataStore;
13
14 /**
15 * Class to import Products from Zoho Inventory to WooCommerce.
16 *
17 * Handles syncing of simple products, variable products, grouped items,
18 * and composite items from Zoho Inventory to WooCommerce.
19 *
20 * @package zoho_inventory_api
21 */
22 class CMBIRD_Products_ZI {
23
24 /**
25 * Configuration array for Zoho Inventory and WooCommerce settings.
26 *
27 * @var array
28 */
29 private $config;
30
31 /**
32 * Flag indicating whether WooCommerce taxes are enabled.
33 *
34 * @var bool
35 */
36 private $is_tax_enabled;
37
38 /**
39 * Constructor to initialize Zoho Inventory and WooCommerce configuration.
40 *
41 * Sets up API credentials, sync settings, and WooCommerce price formatting options.
42 * Exits early if WooCommerce is not active.
43 */
44 public function __construct() {
45 // return if WooCommerce plugin is not active.
46 if ( ! is_plugin_active( 'woocommerce/woocommerce.php' ) ) {
47 return;
48 }
49
50 $this->config = array(
51 'ProductZI' => array(
52 'OID' => get_option( 'cmbird_zoho_inventory_oid' ),
53 'APIURL' => get_option( 'cmbird_zoho_inventory_url' ),
54 ),
55 'Settings' => array(
56 'disable_description' => get_option( 'cmbird_zoho_disable_description_sync_status' ),
57 'disable_name' => get_option( 'cmbird_zoho_disable_name_sync_status' ),
58 'disable_price' => get_option( 'cmbird_zoho_disable_price_sync_status' ),
59 'disable_stock' => get_option( 'cmbird_zoho_disable_stock_sync_status' ),
60 'enable_accounting_stock' => get_option( 'cmbird_zoho_enable_accounting_stock_status' ),
61 'enable_location_stock' => get_option( 'cmbird_zoho_enable_locationstock_status' ),
62 'zoho_location_id' => get_option( 'cmbird_zoho_location_id_status' ),
63 'disable_image' => get_option( 'cmbird_zoho_disable_image_sync_status' ),
64 ),
65 );
66
67 // Check if WooCommerce taxes are enabled and store the result.
68 $this->is_tax_enabled = 'yes' === get_option( 'woocommerce_calc_taxes' );
69 }
70
71 /**
72 * Update or Create Custom Fields of Product
73 *
74 * @param array|object $custom_fields - item object coming in from simple item recursive.
75 * @param int $pdt_id - product id.
76 * @return void
77 */
78 public function sync_item_custom_fields( $custom_fields, $pdt_id ) {
79 if ( empty( $custom_fields ) || empty( $pdt_id ) ) {
80 return;
81 }
82
83 foreach ( $custom_fields as $custom_field ) {
84 // Extract data from custom field.
85 $api_name = isset( $custom_field->api_name ) ? $custom_field->api_name : $custom_field['api_name'];
86 $value = isset( $custom_field->value ) ? $custom_field->value : $custom_field['value'];
87
88 // Check if both API name and value are present.
89 if ( ! empty( $api_name ) && ! empty( $value ) ) {
90 // Check if ACF function exists.
91 if ( function_exists( 'update_field' ) ) {
92 // Update ACF field.
93 update_field( $api_name, $value, $pdt_id );
94 } else {
95 // Fall back to update post meta.
96 update_post_meta( $pdt_id, $api_name, $value );
97 }
98 }
99 }
100 }
101
102
103 /**
104 * Function to add group items recursively by manual sync
105 *
106 * Accepts arguments via func_get_args(): page number and category ID for pagination.
107 *
108 * @return mixed
109 */
110 public function sync_groupitem_recursively() {
111 // $fd = fopen( __DIR__ . '/sync_groupitem_recursively.txt', 'a+' );
112
113 $args = func_get_args();
114 if ( ! empty( $args ) ) {
115 if ( is_array( $args ) ) {
116 if ( isset( $args['page'] ) && isset( $args['category'] ) ) {
117 $page = $args['page'];
118 $category = $args['category'];
119 } elseif ( isset( $args[0] ) && isset( $args[1] ) ) {
120 $page = $args[0];
121 $category = $args[1];
122 } elseif ( isset( $args[0] ) && ! isset( $args[1] ) ) {
123 $page = $args[0]['page'];
124 $category = $args[0]['category'];
125 } else {
126 return;
127 }
128 } else {
129 return;
130 }
131
132 // Memory management: Set memory limit if needed.
133 if ( function_exists( 'wp_raise_memory_limit' ) ) {
134 wp_raise_memory_limit( 'admin' );
135 }
136
137 global $wpdb;
138 $zoho_inventory_oid = $this->config['ProductZI']['OID'];
139 $zoho_inventory_url = $this->config['ProductZI']['APIURL'];
140 $url = $zoho_inventory_url . 'inventory/v1/itemgroups/?organization_id=' . $zoho_inventory_oid . '&category_id=' . $category . '&page=' . $page . '&per_page=20&filter_by=Status.Active';
141 $execute_curl_call = new CMBIRD_API_Handler_Zoho();
142 $json = $execute_curl_call->execute_curl_call_get( $url );
143 $code = $json->code;
144 // $message = $json->message;
145 $response_msg = array();
146
147 if ( '0' === $code || 0 === $code ) {
148 $zi_disable_description_sync = $this->config['Settings']['disable_description'];
149 $zi_disable_name_sync = $this->config['Settings']['disable_name'];
150 // Find the term_id by searching for the option where value matches the category.
151 global $wpdb;
152 $term_id = null;
153 if ( ! empty( $category ) ) {
154 // Query directly for the matching option.
155 $option_name = $wpdb->get_var( $wpdb->prepare( "SELECT option_name FROM $wpdb->options WHERE option_name LIKE %s AND option_value = %s LIMIT 1", 'cmbird_zoho_id_for_term_id_%', $category ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Need to find category mapping.
156 if ( ! empty( $option_name ) ) {
157 // Extract term_id from option_name (e.g., 'cmbird_zoho_id_for_term_id_37' -> '37').
158 $term_id = str_replace( 'cmbird_zoho_id_for_term_id_', '', $option_name );
159 }
160 }
161 $processed_count = 0;
162 foreach ( $json->itemgroups as $gp_arr ) {
163 // Memory cleanup every 10 group items.
164 if ( $processed_count > 0 && $processed_count % 10 === 0 ) {
165 wp_cache_flush();
166 if ( function_exists( 'gc_collect_cycles' ) ) {
167 gc_collect_cycles();
168 }
169 }
170 $zi_group_id = $gp_arr->group_id;
171 $zi_group_name = $gp_arr->group_name;
172 // skip if there is no first attribute.
173 $zi_group_attribute1 = $gp_arr->attribute_id1;
174 if ( empty( $zi_group_attribute1 ) ) {
175 continue;
176 }
177
178 // Get Group ID.
179 $group_id = $wpdb->get_var( $wpdb->prepare( "SELECT post_id FROM $wpdb->postmeta WHERE meta_key = 'zi_item_id' AND meta_value = %s LIMIT 1", $zi_group_id ) );
180 if ( empty( $group_id ) ) {
181 // search by zi_group_name if not found by zi_item_id.
182 $group_id = $wpdb->get_var( $wpdb->prepare( "SELECT p.ID FROM $wpdb->posts p WHERE p.post_title = %s AND p.post_type = 'product' AND p.post_status IN ('publish', 'draft', 'private') LIMIT 1", $zi_group_name ) );
183 }
184 // array_push( $response_msg, $this->zi_response_message( 'SUCCESS', 'Zoho Group Item Synced: ' . $zi_group_name, $group_id ) );
185 // end insert group product.
186 // variable items.
187 if ( ! empty( $group_id ) ) {
188 $existing_parent_product = wc_get_product( $group_id );
189 if ( ! empty( $gp_arr->description ) && ! $zi_disable_description_sync ) {
190 $existing_parent_product->set_short_description( $gp_arr->description );
191 }
192 if ( ! empty( $gp_arr->name ) && ! $zi_disable_name_sync ) {
193 $existing_parent_product->set_name( $gp_arr->name );
194 // santize the name for slug and save the slug.
195 $slug = sanitize_title( $gp_arr->name );
196 $existing_parent_product->set_slug( $slug );
197 }
198 // add zi_category_id as meta.
199 $existing_parent_product->update_meta_data( 'zi_category_id', $gp_arr->category_id );
200 if ( ! empty( $term_id ) ) {
201 wp_set_object_terms( $group_id, (int) $term_id, 'product_cat' );
202 // remove the uncategorized term from the product.
203 wp_remove_object_terms( $group_id, 'uncategorized', 'product_cat' );
204 }
205 // create attributes if not exists.
206 $attributes = $existing_parent_product->get_attributes();
207 if ( empty( $attributes ) ) {
208 // Create or Update the Attributes.
209 $attr_created = $this->sync_attributes_of_group( $gp_arr, $group_id );
210 }
211 $variations_check = $existing_parent_product->get_children();
212 if ( empty( $variations_check ) ) {
213 $this->import_variable_product_variations( $gp_arr, $group_id );
214 }
215 $existing_parent_product->save();
216 // ACF Fields.
217 if ( ! empty( $gp_arr->custom_fields ) ) {
218 $this->sync_item_custom_fields( $gp_arr->custom_fields, $group_id );
219 }
220 // update Brand.
221 if ( ! empty( $gp_arr->brand ) ) {
222 // check if the Brand or Brands taxonomy exists and then update the term.
223 if ( taxonomy_exists( 'product_brand' ) ) {
224 wp_set_object_terms( $group_id, $gp_arr->brand, 'product_brand' );
225 } elseif ( taxonomy_exists( 'product_brand' ) ) {
226 wp_set_object_terms( $group_id, $gp_arr->brand, 'product_brand' );
227 }
228 }
229 } else {
230 // Create the parent variable product.
231 $parent_product = new WC_Product_Variable();
232 $parent_product->set_name( $zi_group_name );
233 $parent_product->set_status( 'publish' );
234 $parent_product->set_short_description( $gp_arr->description );
235 $parent_product->add_meta_data( 'zi_item_id', $zi_group_id );
236 $parent_product->add_meta_data( 'zi_category_id', $category );
237 $group_id = $parent_product->save();
238 // Sync category by finding it first.
239 if ( ! empty( $term_id ) && term_exists( $term_id, 'product_cat' ) ) {
240 wp_set_object_terms( $group_id, (int) $term_id, 'product_cat' );
241 }
242 // Add category.
243 if ( ! empty( $term_id ) ) {
244 wp_set_object_terms( $group_id, (int) $term_id, 'product_cat' );
245 }
246 // update Brand.
247 if ( ! empty( $gp_arr->brand ) ) {
248 // check if the Brand or Brands taxonomy exists and then update the term.
249 if ( taxonomy_exists( 'product_brand' ) ) {
250 wp_set_object_terms( $group_id, $gp_arr->brand, 'product_brand' );
251 } elseif ( taxonomy_exists( 'product_brand' ) ) {
252 wp_set_object_terms( $group_id, $gp_arr->brand, 'product_brand' );
253 }
254 }
255 // ACF Fields.
256 if ( ! empty( $gp_arr->custom_fields ) ) {
257 $this->sync_item_custom_fields( $gp_arr->custom_fields, $group_id );
258 }
259 // Create or Update the Attributes.
260 $attr_created = $this->sync_attributes_of_group( $gp_arr, $group_id );
261
262 if ( ! empty( $group_id ) && $attr_created ) {
263 $this->import_variable_product_variations( $gp_arr, $group_id );
264 }
265 } // end of create variable product.
266
267 ++$processed_count;
268 } // end foreach group items.
269
270 // Final memory cleanup after processing all group items.
271 wp_cache_flush();
272 if ( function_exists( 'gc_collect_cycles' ) ) {
273 gc_collect_cycles();
274 }
275
276 // Schedule next page if available.
277 if ( isset( $json->page_context ) && $json->page_context->has_more_page ) {
278 $data = array(
279 'page' => $page + 1,
280 'category' => $category,
281 );
282 $existing_schedule = as_has_scheduled_action( 'import_group_items_cron', $data, 'commercebird' );
283 // Check if the scheduled action exists.
284 if ( ! $existing_schedule ) {
285 as_schedule_single_action( time(), 'import_group_items_cron', $data, 'commercebird' );
286 }
287 }
288
289 // Store message before unsetting json.
290 $message = isset( $json->message ) ? $json->message : '';
291 // Clear any temporary variables.
292 unset( $json, $execute_curl_call );
293 array_push( $response_msg, $this->zi_response_message( $code, $message ) );
294 }
295 // End of logging.
296 // fclose( $fd );
297 return $response_msg;
298 } else {
299 return;
300 }
301 }
302
303 /**
304 * Callback function for importing a variable product and its variations.
305 *
306 * @param object $gp_arr - Group item object.
307 * @param int $group_id - Group item id.
308 * @return void
309 */
310 public function import_variable_product_variations( $gp_arr, $group_id ): void {
311 // $fd = fopen( __DIR__ . '/import_variable_product_variations.txt', 'a+' );
312
313 if ( empty( $gp_arr ) || empty( $group_id ) ) {
314 return;
315 }
316
317 global $wpdb;
318 $product = wc_get_product( $group_id );
319
320 if ( $product ) {
321 $item_group = $gp_arr;
322 $items = $item_group->items;
323 $attribute_name1 = $item_group->attribute_name1;
324 $attribute_name2 = $item_group->attribute_name2;
325 $attribute_name3 = $item_group->attribute_name3;
326
327 // fwrite( $fd, PHP_EOL . 'Items : ' . print_r( $items, true ) );
328 // get the options for stock sync.
329 $zi_enable_locationstock = $this->config['Settings']['enable_location_stock'];
330 $location_id = $this->config['Settings']['zoho_location_id'];
331 $accounting_stock = $this->config['Settings']['enable_accounting_stock'];
332 $zi_disable_stock_sync = $this->config['Settings']['disable_stock'];
333 $zi_disable_image_sync = $this->config['Settings']['disable_image'];
334 $locations = array();
335
336 $to_create = array();
337 $batch_count = 0;
338 $max_batch_size = 100;
339 $parent_image_set = false;
340
341 foreach ( $items as $item ) {
342 // reset this array.
343 $attribute_arr = array();
344 $variation_id = '';
345 $status = $item->status === 'active' ? 'publish' : 'draft';
346
347 $zi_item_id = $item->item_id;
348 $variation_id = $wpdb->get_var( $wpdb->prepare( "SELECT post_id FROM $wpdb->postmeta WHERE meta_key = 'zi_item_id' AND meta_value = %s LIMIT 1", $zi_item_id ) );
349
350 if ( ! empty( $variation_id ) ) {
351 $v_product = wc_get_product( $variation_id );
352 // Check if the product object is valid.
353 if ( $v_product && is_a( $v_product, 'WC_Product' ) ) {
354 if ( $v_product->is_type( 'simple' ) ) {
355 wp_delete_post( $variation_id, true );
356 }
357 }
358 }
359 // SKU check of the variation, if exits then remove it.
360 if ( ! empty( $item->sku ) ) {
361 $sku_prod_id = wc_get_product_id_by_sku( $item->sku );
362 $v_product = wc_get_product( $sku_prod_id );
363 // Check if the product object is valid.
364 if ( $v_product && is_a( $v_product, 'WC_Product' ) ) {
365 if ( $v_product->is_type( 'simple' ) ) {
366 wp_delete_post( $variation_id, true );
367 }
368 }
369 }
370 if ( ! empty( $variation_id ) ) {
371 continue;
372 }
373 // Stock mode check.
374 $locations = $item->locations;
375 $stock = 0;
376
377 if ( $zi_enable_locationstock && $location_id ) {
378 foreach ( $locations as $location ) {
379 if ( $location->location_id === $location_id ) {
380 if ( $accounting_stock ) {
381 $stock = isset( $location->location_available_stock ) ? $location->location_available_stock : 0;
382 } else {
383 $stock = isset( $location->location_actual_available_stock ) ? $location->location_actual_available_stock : 0;
384 }
385 }
386 }
387 } elseif ( $accounting_stock ) {
388 $stock = $item->available_stock;
389 } else {
390 $stock = $item->actual_available_stock;
391 }
392
393 $attribute_name11 = $item->attribute_option_name1;
394 $attribute_name12 = $item->attribute_option_name2;
395 $attribute_name13 = $item->attribute_option_name3;
396 // fwrite( $fd, PHP_EOL . '>>> Item: ' . $item->item_id . ' | SKU: ' . $item->sku . ' | opt1: ' . $attribute_name11 . ' | opt2: ' . $attribute_name12 . ' | opt3: ' . $attribute_name13 );
397 // Prepare the variation data.
398 if ( ! empty( $attribute_name1 ) ) {
399 $sanitized_name1 = wc_sanitize_taxonomy_name( $attribute_name1 );
400 $attribute_arr[ $sanitized_name1 ] = $attribute_name11;
401 }
402 if ( ! empty( $attribute_name2 ) ) {
403 $sanitized_name2 = wc_sanitize_taxonomy_name( $attribute_name2 );
404 $attribute_arr[ $sanitized_name2 ] = $attribute_name12;
405 }
406 if ( ! empty( $attribute_name3 ) ) {
407 $sanitized_name3 = wc_sanitize_taxonomy_name( $attribute_name3 );
408 $attribute_arr[ $sanitized_name3 ] = $attribute_name13;
409 }
410 // fwrite( $fd, PHP_EOL . '$attribute_arr : ' . print_r( $attribute_arr, true ) );
411
412 // Process and set variation attributes.
413 $variation_attributes = array();
414 foreach ( $attribute_arr as $attribute => $term_name ) {
415 $taxonomy = 'pa_' . $attribute;
416
417 // If taxonomy doesn't exist, create it.
418 if ( ! taxonomy_exists( $taxonomy ) ) {
419 register_taxonomy(
420 $taxonomy,
421 'product_variation',
422 array(
423 'hierarchical' => false,
424 'label' => ucfirst( $attribute ),
425 'query_var' => true,
426 'rewrite' => array( 'slug' => sanitize_title( $attribute ) ),
427 ),
428 );
429 }
430
431 // Check if the term exists and create if needed.
432 if ( ! term_exists( $term_name, $taxonomy ) ) {
433 wp_insert_term( $term_name, $taxonomy );
434 }
435
436 // Get the term slug.
437 $term_object = get_term_by( 'name', $term_name, $taxonomy );
438 $term_slug = $term_object ? $term_object->slug : sanitize_title( $term_name );
439
440 // Get the post terms from the parent variable product.
441 $post_term_names = wp_get_post_terms( $group_id, $taxonomy, array( 'fields' => 'names' ) );
442
443 // Set the term on the parent product if not already set.
444 if ( ! in_array( $term_name, $post_term_names, true ) ) {
445 wp_set_post_terms( $group_id, $term_name, $taxonomy, true );
446 }
447
448 // Build variation attributes array for batch API.
449 $variation_attributes[ $taxonomy ] = array(
450 'slug' => $term_slug,
451 'name' => $term_name,
452 );
453 }
454
455 // Prepare variation data for batch API.
456 $variation_data = array(
457 'type' => 'variation',
458 'status' => $status,
459 'regular_price' => (string) $item->rate,
460 'sku' => $item->sku,
461 );
462
463 // Set attributes.
464 if ( ! empty( $variation_attributes ) ) {
465 $variation_data['attributes'] = array();
466 foreach ( $variation_attributes as $taxonomy => $term_data ) {
467 $variation_data['attributes'][] = array(
468 'id' => wc_attribute_taxonomy_id_by_name( str_replace( 'pa_', '', $taxonomy ) ),
469 'option' => $term_data['name'],
470 );
471 }
472 }
473
474 // Handle stock.
475 if ( ! $zi_disable_stock_sync && $stock > 0 ) {
476 $variation_data['manage_stock'] = true;
477 $variation_data['stock_quantity'] = $stock;
478 } else {
479 $variation_data['manage_stock'] = false;
480 }
481
482 // Handle image.
483 if ( ! empty( $item->image_name ) && ! $zi_disable_image_sync ) {
484 $image_class = new CMBIRD_Image_ZI();
485 $attachment_id = $image_class->cmbird_zi_get_image( $item->item_id, $item->name, $item->image_name, null );
486 if ( $attachment_id ) {
487 $variation_data['image'] = array( 'id' => $attachment_id );
488 // Set parent product thumbnail if not already set.
489 if ( ! $parent_image_set && ! has_post_thumbnail( $group_id ) ) {
490 set_post_thumbnail( $group_id, $attachment_id );
491 $parent_image_set = true;
492 }
493 }
494 }
495
496 // Add meta data.
497 $variation_data['meta_data'] = array(
498 array(
499 'key' => 'zi_item_id',
500 'value' => $item->item_id,
501 ),
502 );
503
504 // Add purchase price if available.
505 if ( ! empty( $item->purchase_rate ) ) {
506 $variation_data['meta_data'][] = array(
507 'key' => 'cogs_total_value',
508 'value' => $item->purchase_rate,
509 );
510 }
511
512 // Add to batch array.
513 $to_create[] = $variation_data;
514 ++$batch_count;
515
516 // Process batch when we hit the limit.
517 if ( $batch_count >= $max_batch_size ) {
518 \CommerceBird\Admin\Actions\Sync\ZohoInventorySync::import(
519 'product',
520 array( 'create' => $to_create ),
521 false,
522 '/wc/v3/products/' . $group_id . '/variations/batch'
523 );
524 $to_create = array();
525 $batch_count = 0;
526 }
527 }
528
529 // Process remaining variations in the batch.
530 if ( ! empty( $to_create ) ) {
531 \CommerceBird\Admin\Actions\Sync\ZohoInventorySync::import(
532 'product',
533 $to_create,
534 false,
535 '/wc/v3/products/' . $group_id . '/variations/batch'
536 );
537 }
538 // End group item add process.
539 // array_push($response_msg, $this->zi_response_message('SUCCESS', 'Zoho variable item created for zoho item id ' . $zi_item_id, $variation_id));
540 // End of Logging.
541 // fclose( $fd );
542 }
543 }
544
545 /**
546 * Update or Create the Product Attributes for the Variable Item Sync
547 *
548 * @param object $gp_arr - the group item object from Zoho Inventory.
549 * @param int $group_id - the parent product id in WooCommerce.
550 * @return bool - true if attributes were created successfully, false otherwise
551 */
552 public function sync_attributes_of_group( $gp_arr, $group_id ) {
553 // $fd = fopen( __DIR__ . '/sync_attributes_of_group.txt', 'a+' );
554 // Check if the group item has attributes.
555 if ( empty( $gp_arr->attribute_name1 ) ) {
556 return false;
557 }
558 // Create attributes.
559 $success = true; // Track the success of attribute creation.
560 $attributes_data = array();
561 $attribute_count = 0;
562
563 // Loop through the attribute names.
564 for ( $i = 1; $i <= 3; $i++ ) {
565 $attribute_name_key = 'attribute_name' . $i;
566 $attribute_option_name_key = 'attribute_option_name' . $i;
567
568 // Get the attribute name.
569 $attribute_name = $gp_arr->$attribute_name_key;
570
571 if ( ! empty( $attribute_name ) ) {
572 // Check if the attribute is already added to the attributes array.
573 if ( ! isset( $attributes_data[ $attribute_name ] ) ) {
574 // Create the attribute and add it to the attributes array.
575 $attribute = array(
576 'name' => $attribute_name,
577 'position' => $attribute_count,
578 'visible' => true,
579 'variation' => true,
580 'options' => array(),
581 );
582
583 // Loop through the items and retrieve attribute options.
584 $attribute_options = array();
585 foreach ( $gp_arr->items as $item ) {
586 $attribute_option = $item->$attribute_option_name_key;
587 if ( ! empty( $attribute_option ) && ! in_array( $attribute_option, $attribute_options, true ) ) {
588 $attribute_options[] = $attribute_option;
589 }
590 }
591
592 // Set the attribute options.
593 $attribute['options'] = $attribute_options;
594
595 $attributes_data[] = $attribute;
596 ++$attribute_count;
597 }
598 }
599 }
600 // fwrite( $fd, PHP_EOL . '$attributes : ' . print_r( $attributes_data, true ) );
601
602 // Assign the attributes to the parent product.
603 if ( count( $attributes_data ) > 0 ) {
604 $product_attributes = array();
605
606 // Loop through defined attribute data.
607 foreach ( $attributes_data as $key => $attribute_array ) {
608 if ( isset( $attribute_array['name'] ) && isset( $attribute_array['options'] ) ) {
609 // Clean attribute name to get the taxonomy.
610 $taxonomy = 'pa_' . wc_sanitize_taxonomy_name( $attribute_array['name'] );
611 $option_term_ids = array();
612 $existing_terms = array();
613 // Create the attribute if it doesn't exist.
614 if ( ! taxonomy_exists( $taxonomy ) ) {
615 // Clean attribute label for better display.
616 $attribute_label = ucfirst( $attribute_array['name'] );
617
618 // Register the new attribute taxonomy.
619 $attribute_args = array(
620 'slug' => $taxonomy,
621 'name' => $attribute_label,
622 'type' => 'select',
623 'order_by' => 'menu_order',
624 'has_archives' => false,
625 );
626
627 $result = wc_create_attribute( $attribute_args );
628 register_taxonomy( $taxonomy, array( 'product' ), array() );
629
630 if ( ! is_wp_error( $result ) ) {
631 // fwrite($fd, PHP_EOL . 'result : ' . $result);
632 // Loop through defined attribute data options (terms values).
633 foreach ( $attribute_array['options'] as $option ) {
634 // Check if the term exists for the attribute taxonomy.
635 $term = term_exists( $option, $taxonomy );
636 // Also check by slug in case option is slug-formatted.
637 $term_by_slug = get_term_by( 'slug', sanitize_title( $option ), $taxonomy );
638 if ( $term_by_slug ) {
639 $term = array( 'term_id' => $term_by_slug->term_id );
640 }
641 if ( ! $term ) {
642 // Term doesn't exist, create a new one.
643 // Convert slug-formatted names to proper names.
644 $term_name = str_replace( array( '-', '_' ), ' ', $option );
645 $term_name = ucwords( $term_name );
646 $term = wp_insert_term(
647 $term_name,
648 $taxonomy,
649 array(
650 'slug' => sanitize_title( $term_name ),
651 )
652 );
653 if ( is_wp_error( $term ) ) {
654 // fwrite( $fd, PHP_EOL . 'Error creating term: ' . $term->get_error_message() );
655 continue;
656 }
657 }
658 // Add term ID to the array for assignment to parent product.
659 $term_id = is_array( $term ) ? $term['term_id'] : $term;
660 $option_term_ids[] = $term_id;
661 }
662 } else {
663 $success = false;
664 // return error message.
665 $error_string = $result->get_error_message();
666 return $success;
667 }
668 } else {
669 // Taxonomy exists, get existing terms.
670 $existing_terms = get_terms(
671 array(
672 'taxonomy' => $taxonomy,
673 'hide_empty' => false,
674 )
675 );
676 }
677
678 if ( $existing_terms ) {
679 foreach ( $attribute_array['options'] as $option ) {
680 $match_found = false;
681 foreach ( $existing_terms as $existing_term ) {
682 // Check both name and slug to handle cases where Zoho sends slug-formatted names.
683 if ( $existing_term->name === $option || $existing_term->slug === sanitize_title( $option ) ) {
684 $option_term_ids[] = $existing_term->term_id;
685 $match_found = true;
686 break;
687 }
688 }
689 // fwrite( $fd, PHP_EOL . 'Option "' . $option . '" match found: ' . ( $match_found ? 'Yes' : 'No' ) );
690 if ( ! $match_found ) {
691 // Check if the term exists for the attribute taxonomy.
692 $term = get_term_by( 'slug', sanitize_title( $option ), $taxonomy );
693 // fwrite( $fd, PHP_EOL . 'Term by slug check for option "' . $option . '": ' . print_r( $term, true ) );
694 if ( $term ) {
695 $term = array( 'term_id' => $term->term_id );
696 }
697 if ( ! $term ) {
698 // Term doesn't exist, create it.
699 // Convert slug-formatted names to proper names.
700 $term_name = str_replace( array( '-', '_' ), ' ', $option );
701 $term_name = ucwords( $term_name );
702 $term = wp_insert_term( $term_name, $taxonomy );
703 if ( ! is_wp_error( $term ) ) {
704 // Get the term ID.
705 $term_id = is_array( $term ) ? $term['term_id'] : $term;
706 $option_term_ids[] = $term_id;
707 } else {
708 $success = false;
709 }
710 } else {
711 // Get the existing term ID.
712 $term_id = $term['term_id'];
713 $option_term_ids[] = $term_id;
714 }
715 }
716 }
717 }
718 // Set the selected terms for the product.
719 // fwrite( $fd, PHP_EOL . '[SYNC_ATTR] Setting terms for taxonomy: ' . $taxonomy . ' | Term IDs: ' . print_r( $option_term_ids, true ) );
720 wp_set_object_terms( $group_id, $option_term_ids, $taxonomy, false );
721
722 $attribute_object = new WC_Product_Attribute();
723 $attribute_object->set_id( wc_attribute_taxonomy_id_by_name( $taxonomy ) );
724 $attribute_object->set_name( $taxonomy );
725 $attribute_object->set_options( $option_term_ids );
726 $attribute_object->set_visible( $attribute_array['visible'] ?? true );
727 $attribute_object->set_variation( $attribute_array['variation'] ?? true );
728 $product_attributes[] = $attribute_object;
729 }
730 }
731 }
732
733 // Save the attributes to the parent product using WooCommerce 10+ API.
734 if ( ! empty( $product_attributes ) ) {
735 $product = wc_get_product( $group_id );
736 if ( $product ) {
737 $product->set_attributes( $product_attributes );
738 $product->save();
739 }
740 }
741 // fclose($fd);
742 return $success;
743 }
744
745 /**
746 * Update or create variation in WooCommerce if Group-ID already exists in wpdB
747 *
748 * @param object $item - item object coming in from simple item recursive function.
749 * @return: void
750 */
751 public function sync_variation_of_group( $item ) {
752 // $fd = fopen( __DIR__ . '/sync_variation_of_group.txt', 'a+' );
753
754 // log the item.
755 // fwrite( $fd, PHP_EOL . 'Item : ' . print_r( $item, true ) );
756
757 global $wpdb;
758 // Stock mode check.
759 $zi_disable_stock_sync = $this->config['Settings']['disable_stock'];
760 $accounting_stock = $this->config['Settings']['enable_accounting_stock'];
761 $is_tax_enabled = $this->is_tax_enabled;
762
763 if ( $accounting_stock ) {
764 $stock = $item->available_stock;
765 } else {
766 $stock = $item->actual_available_stock;
767 }
768
769 $item_id = $item->item_id;
770 // $item_category = $item->category_name;
771 $groupid = property_exists( $item, 'group_id' ) ? $item->group_id : 0;
772 // find parent variable product.
773 $group_id = $wpdb->get_var( $wpdb->prepare( "SELECT post_id FROM $wpdb->postmeta WHERE meta_key = 'zi_item_id' AND meta_value = %s LIMIT 1", $groupid ) );
774 // fwrite($fd, PHP_EOL . 'Row Data : ' . print_r($row, true));
775 // fwrite($fd, PHP_EOL . 'Row $group_id : ' . $group_id);
776 $stock_quantity = $stock < 0 ? 0 : $stock;
777 // fwrite($fd, PHP_EOL . 'Before group item sync : ' . $group_id);
778 if ( ! empty( $group_id ) ) {
779 // fwrite( $fd, PHP_EOL . 'Inside item sync : ' . $item->name );
780 // Brand.
781 if ( isset( $item->brand ) && ! empty( $group_id ) ) {
782 if ( taxonomy_exists( 'product_brand' ) ) {
783 wp_set_object_terms( $group_id, $item->brand, 'product_brand' );
784 }
785 }
786
787 $variation_id = $wpdb->get_var( $wpdb->prepare( "SELECT post_id FROM $wpdb->postmeta WHERE meta_key = 'zi_item_id' AND meta_value = %s LIMIT 1", $item_id ) );
788 if ( ! empty( $item->sku ) && empty( $variation_id ) ) {
789 $variation_id = wc_get_product_id_by_sku( $item->sku );
790 }
791 if ( $variation_id ) {
792 // fwrite( $fd, PHP_EOL . 'Variation ID exists : ' . $variation_id );
793 // if product type is not variation then return.
794 $v_product = wc_get_product( $variation_id );
795 // Check if the product object is valid.
796 if ( $v_product && is_a( $v_product, 'WC_Product' ) ) {
797 if ( $v_product->is_type( 'simple' ) ) {
798 wp_delete_post( $variation_id, true );
799 $variation_id = '';
800 // Continue to create new variation below.
801 }
802 }
803 // If variation already exists then update it.
804 $variation = new WC_Product_Variation( $variation_id );
805 // SKU - Imported.
806 if ( ! empty( $item->sku ) ) {
807 $variation->set_sku( $item->sku );
808 }
809 // update purchase price as meta data.
810 if ( ! empty( $item->purchase_rate ) ) {
811 $variation->update_meta_data( 'cogs_total_value', $item->purchase_rate );
812 }
813 // Price - Imported.
814 $zi_disable_price_sync = $this->config['Settings']['disable_price'];
815 $variation_sale_price = $variation->get_sale_price();
816 if ( empty( $variation_sale_price ) && ! $zi_disable_price_sync ) {
817 $variation->set_sale_price( $item->rate );
818 }
819 $variation->set_regular_price( $item->rate );
820 // Set Tax Class.
821 if ( $item->tax_id && $is_tax_enabled ) {
822 $zi_common_class = new CMBIRD_Common_Functions();
823 $woo_tax_class = $zi_common_class->get_tax_class_by_percentage( $item->tax_percentage );
824 $variation->set_tax_status( 'taxable' );
825 $variation->set_tax_class( $woo_tax_class );
826 }
827 // Stock Imported code.
828 if ( ! $zi_disable_stock_sync && is_numeric( $stock_quantity ) ) {
829 // fwrite( $fd, PHP_EOL . 'Stock Quantity : ' . $stock_quantity );
830 $variation->set_manage_stock( true );
831 if ( $stock_quantity > 0 ) {
832 $variation->set_manage_stock( true );
833 $variation->set_stock_quantity( $stock_quantity );
834 $variation->set_stock_status( 'instock' );
835 } elseif ( $stock_quantity <= 0 ) {
836 $variation->set_manage_stock( true );
837 $variation->set_stock_quantity( $stock_quantity );
838 $stock_status = $variation->backorders_allowed() ? 'onbackorder' : 'outofstock';
839 $variation->set_stock_status( $stock_status );
840 }
841 }
842 // Update variation attributes if missing or need updating.
843 $attribute_name1 = $item->attribute_option_name1;
844 $attribute_name2 = $item->attribute_option_name2;
845 $attribute_name3 = $item->attribute_option_name3;
846
847 // Prepare the variation data.
848 $attribute_arr = array();
849 if ( ! empty( $attribute_name1 ) ) {
850 $sanitized_name1 = wc_sanitize_taxonomy_name( $item->attribute_name1 );
851 $attribute_arr[ $sanitized_name1 ] = $attribute_name1;
852 }
853 if ( ! empty( $attribute_name2 ) ) {
854 $sanitized_name2 = wc_sanitize_taxonomy_name( $item->attribute_name2 );
855 $attribute_arr[ $sanitized_name2 ] = $attribute_name2;
856 }
857 if ( ! empty( $attribute_name3 ) ) {
858 $sanitized_name3 = wc_sanitize_taxonomy_name( $item->attribute_name3 );
859 $attribute_arr[ $sanitized_name3 ] = $attribute_name3;
860 }
861
862 // Update attributes for the variation.
863 $variation_attributes = array();
864 foreach ( $attribute_arr as $attribute => $term_name ) {
865 $taxonomy = 'pa_' . $attribute;
866
867 // If taxonomy doesn't exist, create it.
868 if ( ! taxonomy_exists( $taxonomy ) ) {
869 register_taxonomy(
870 $taxonomy,
871 'product_variation',
872 array(
873 'hierarchical' => false,
874 'label' => ucfirst( $attribute ),
875 'query_var' => true,
876 'rewrite' => array( 'slug' => sanitize_title( $attribute ) ),
877 ),
878 );
879 }
880
881 // Check if the term exists and create if needed.
882 if ( ! term_exists( $term_name, $taxonomy ) ) {
883 wp_insert_term( $term_name, $taxonomy );
884 }
885
886 // Get the term slug.
887 $term_object = get_term_by( 'name', $term_name, $taxonomy );
888 $term_slug = $term_object ? $term_object->slug : sanitize_title( $term_name );
889
890 // Get the post terms from the parent variable product.
891 $post_term_names = wp_get_post_terms( $group_id, $taxonomy, array( 'fields' => 'names' ) );
892
893 // Set the term on the parent product if not already set.
894 if ( ! in_array( $term_name, $post_term_names, true ) ) {
895 wp_set_post_terms( $group_id, $term_name, $taxonomy, true );
896 }
897
898 // Build variation attributes array for set_attributes().
899 $variation_attributes[ $taxonomy ] = $term_slug;
900 }
901
902 // Set all attributes at once using WooCommerce 10+ API.
903 if ( ! empty( $variation_attributes ) ) {
904 $variation->set_attributes( $variation_attributes );
905 }
906
907 // Featured Image of variation.
908 if ( ! empty( $item->image_document_id ) ) {
909 $image_class = new CMBIRD_Image_ZI();
910 $variation_image_id = $image_class->cmbird_zi_get_image( $item->item_id, $item->name, $item->image_name, $variation_id );
911 if ( ! has_post_thumbnail( $group_id ) ) {
912 if ( $variation_image_id ) {
913 set_post_thumbnail( $group_id, $variation_image_id );
914 }
915 }
916 }
917 // enable or disable based on status from Zoho.
918 $status = ( 'active' === $item->status ) ? 'publish' : 'draft';
919 $variation->set_status( $status );
920 $variation->save();
921 // clear cache.
922 wc_delete_product_transients( $variation_id );
923 } else {
924 // create new variation.
925 // if status is not active then return.
926 if ( 'active' !== $item->status ) {
927 return;
928 }
929
930 $attribute_name1 = $item->attribute_option_name1;
931 $attribute_name2 = $item->attribute_option_name2;
932 $attribute_name3 = $item->attribute_option_name3;
933 // Prepare the variation data.
934 $attribute_arr = array();
935 if ( ! empty( $attribute_name1 ) ) {
936 $sanitized_name1 = wc_sanitize_taxonomy_name( $item->attribute_name1 );
937 $attribute_arr[ $sanitized_name1 ] = $attribute_name1;
938 }
939 if ( ! empty( $attribute_name2 ) ) {
940 $sanitized_name2 = wc_sanitize_taxonomy_name( $item->attribute_name2 );
941 $attribute_arr[ $sanitized_name2 ] = $attribute_name2;
942 }
943 if ( ! empty( $attribute_name3 ) ) {
944 $sanitized_name3 = wc_sanitize_taxonomy_name( $item->attribute_name3 );
945 $attribute_arr[ $sanitized_name3 ] = $attribute_name3;
946 }
947 // fwrite( $fd, PHP_EOL . 'Attributes_arr: ' . print_r( $attribute_arr, true ) );
948
949 // here actually create new variation because sku not found.
950 $variation = new WC_Product_Variation();
951 $variation->set_parent_id( $group_id );
952 $variation->set_status( 'publish' );
953 $variation->set_regular_price( $item->rate );
954 $variation->set_sku( $item->sku );
955 if ( ! $zi_disable_stock_sync ) {
956 $variation->set_stock_quantity( $stock_quantity );
957 $variation->set_manage_stock( true );
958 $variation->set_stock_status( '' );
959 } else {
960 $variation->set_manage_stock( false );
961 }
962 $variation->add_meta_data( 'zi_item_id', $item->item_id );
963 $variation_id = $variation->save();
964
965 // Get the variation attributes with correct attribute values.
966 $variation_attributes = array();
967 foreach ( $attribute_arr as $attribute => $term_name ) {
968 $taxonomy = 'pa_' . $attribute;
969
970 // If taxonomy doesn't exist, create it.
971 if ( ! taxonomy_exists( $taxonomy ) ) {
972 register_taxonomy(
973 $taxonomy,
974 'product_variation',
975 array(
976 'hierarchical' => false,
977 'label' => ucfirst( $attribute ),
978 'query_var' => true,
979 'rewrite' => array( 'slug' => sanitize_title( $attribute ) ),
980 ),
981 );
982 }
983
984 // Check if the term exists and create if needed.
985 if ( ! term_exists( $term_name, $taxonomy ) ) {
986 wp_insert_term( $term_name, $taxonomy );
987 }
988
989 // Get the term slug.
990 $term_object = get_term_by( 'name', $term_name, $taxonomy );
991 $term_slug = $term_object ? $term_object->slug : sanitize_title( $term_name );
992
993 // Get the post terms from the parent variable product.
994 $post_term_names = wp_get_post_terms( $group_id, $taxonomy, array( 'fields' => 'names' ) );
995
996 // Set the term on the parent product if not already set.
997 if ( ! in_array( $term_name, $post_term_names, true ) ) {
998 wp_set_post_terms( $group_id, $term_name, $taxonomy, true );
999 }
1000
1001 // Build variation attributes array for set_attributes().
1002 $variation_attributes[ $taxonomy ] = $term_slug;
1003 }
1004
1005 // Set all attributes at once using WooCommerce 10+ API.
1006 if ( ! empty( $variation_attributes ) ) {
1007 $variation->set_attributes( $variation_attributes );
1008 $variation->save();
1009 }
1010
1011 // update purchase price as meta data.
1012 if ( ! empty( $item->purchase_rate ) ) {
1013 $variation->update_meta_data( 'cogs_total_value', $item->purchase_rate );
1014 }
1015 // Stock.
1016 if ( ! empty( $stock ) && ! $zi_disable_stock_sync ) {
1017 update_post_meta( $variation_id, 'manage_stock', true );
1018 if ( $stock > 0 ) {
1019 update_post_meta( $variation_id, '_stock', $stock );
1020 update_post_meta( $variation_id, '_stock_status', 'instock' );
1021 } else {
1022 $backorder_status = get_post_meta( $group_id, '_backorders', true );
1023 update_post_meta( $variation_id, '_stock', $stock );
1024 if ( 'yes' === $backorder_status ) {
1025 update_post_meta( $variation_id, '_stock_status', 'onbackorder' );
1026 } else {
1027 update_post_meta( $variation_id, '_stock_status', 'outofstock' );
1028 }
1029 }
1030 }
1031 // Featured Image of variation.
1032 if ( ! empty( $item->image_document_id ) ) {
1033 $image_class = new CMBIRD_Image_ZI();
1034 $variation_image_id = $image_class->cmbird_zi_get_image( $item->item_id, $item->name, $item->image_name, $variation_id );
1035 if ( ! has_post_thumbnail( $group_id ) ) {
1036 if ( $variation_image_id ) {
1037 set_post_thumbnail( $group_id, $variation_image_id );
1038 }
1039 }
1040 }
1041 update_post_meta( $variation_id, 'zi_item_id', $item_id );
1042 // WC_Product_Variable::sync( $group_id );
1043 // Regenerate lookup table for attributes.
1044 $lookup_data_store = new LookupDataStore();
1045 $lookup_data_store->create_data_for_product( $group_id );
1046 // End group item add process.
1047 unset( $attribute_arr );
1048 }
1049 // end of grouped item updating.
1050 }
1051 }
1052
1053 /**
1054 * Helper Function to check if child of composite items already synced or not
1055 *
1056 * @param string $composite_zoho_id - zoho composite item id to check if it's child are already synced.
1057 * @param string $zi_url - zoho api url.
1058 * @param string $zi_org_id - zoho organization token.
1059 * @param string $prod_id - woocommerce product id.
1060 * @return array | bool of child id and metadata if child item already synced else will return false.
1061 */
1062 public function zi_check_if_child_synced_already( $composite_zoho_id, $zi_url, $zi_org_id, $prod_id ) {
1063 if ( $prod_id ) {
1064 $bundle_childs = WC_PB_DB::query_bundled_items(
1065 array(
1066 'return' => 'id=>product_id',
1067 'bundle_id' => array( $prod_id ),
1068 )
1069 );
1070 }
1071 global $wpdb;
1072
1073 $url = $zi_url . 'inventory/v1/compositeitems/' . $composite_zoho_id . '?organization_id=' . $zi_org_id;
1074
1075 $execute_curl_call = new CMBIRD_API_Handler_Zoho();
1076 $json = $execute_curl_call->execute_curl_call_get( $url );
1077 $code = $json->code;
1078 // Flag to allow sync of parent composite item.
1079 $allow_sync = false;
1080 // Array of child object metadata.
1081 $product_array = array(); // [{prod_id:'',metadata:{key:'',value:''}},...].
1082 if ( '0' === $code || 0 === $code ) {
1083 foreach ( $json->composite_item->mapped_items as $child_item ) {
1084 $prod_meta = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $wpdb->postmeta WHERE meta_key = 'zi_item_id' AND meta_value = %s", $child_item->item_id ) );
1085 // If any child will not have zoho id in meta field then process will return false and syncing will be skipped for given item.
1086 if ( ! empty( $prod_meta->post_id ) ) {
1087
1088 $allow_sync = true;
1089 $product = wc_get_product( $prod_meta->post_id );
1090 $stock_status = 'out_of_stock';
1091 if ( $child_item->stock_on_hand > 0 ) {
1092 $stock_status = 'in_stock';
1093 } elseif ( $product && $product->backorders_allowed() ) {
1094 $stock_status = 'onbackorder';
1095 }
1096 $prod_obj = (object) array(
1097 'prod_id' => $prod_meta->post_id,
1098 'metadata' => (object) array(
1099 'quantity_min' => max( 1, $child_item->quantity ),
1100 'quantity_max' => max( 1, $child_item->quantity ),
1101 'stock_status' => $stock_status,
1102 'max_stock' => $child_item->stock_on_hand,
1103 ),
1104 );
1105 if ( is_array( $bundle_childs ) && ! empty( $bundle_childs ) ) {
1106 $index = array_search( $prod_meta->post_id, $bundle_childs );
1107 unset( $bundle_childs[ $index ] );
1108 }
1109 array_push( $product_array, $prod_obj );
1110 } else {
1111 continue;
1112 }
1113 }
1114 }
1115 if ( is_array( $bundle_childs ) && ! empty( $bundle_childs ) ) {
1116 foreach ( $bundle_childs as $item_id => $val ) {
1117 WC_PB_DB::delete_bundled_item( $item_id );
1118 }
1119 }
1120 if ( $allow_sync ) {
1121 return $product_array;
1122 }
1123 return false;
1124 }
1125 /**
1126 * Mapping of bundled product
1127 *
1128 * @param number $product_id - Product id of child item of bundle product.
1129 * @param number $bundle_id - Bundle id of product.
1130 * @param number $menu_order - Listing order of child product ($menu_order will useful at composite product details page).
1131 * @return number - bundle item id.
1132 */
1133 public function add_bundle_product( $product_id, $bundle_id, $menu_order = 0 ) {
1134 $bundle_items = WC_PB_DB::query_bundled_items(
1135 array(
1136 'return' => 'id=>product_id',
1137 'bundle_id' => array( $bundle_id ),
1138 'product_id' => array( $product_id ),
1139 )
1140 );
1141 $data = array(
1142 'menu_order' => $menu_order,
1143 );
1144
1145 if ( count( $bundle_items ) > 0 ) {
1146 $result = WC_PB_DB::update_bundled_item( $bundle_id, $data );
1147 return $result;
1148 } else {
1149 // create data array of bundle item.
1150 $data = array(
1151 'product_id' => $product_id,
1152 'bundle_id' => $bundle_id,
1153 'menu_order' => $menu_order,
1154 );
1155 $bundle_id = WC_PB_DB::add_bundled_item( $data );
1156 return $bundle_id;
1157 }
1158 }
1159
1160 /**
1161 * Create or update bundle item metadata
1162 *
1163 * @param number $bundle_item_id bundle item id.
1164 * @param string $meta_key - metadata key.
1165 * @param string $meta_value - metadata value.
1166 * @return float|int $result - metadata id.
1167 */
1168 public function zi_update_bundle_meta( $bundle_item_id, $meta_key, $meta_value ) {
1169 // first get metadata from db.
1170 $metadata = WC_PB_DB::get_bundled_item_meta( $bundle_item_id, $meta_key );
1171 if ( $metadata ) {
1172 $result = WC_PB_DB::update_bundled_item_meta( $bundle_item_id, $meta_key, $meta_value );
1173 } else {
1174 $result = WC_PB_DB::add_bundled_item_meta( $bundle_item_id, $meta_key, $meta_value );
1175 }
1176 return $result;
1177 }
1178
1179 /**
1180 * Function to sync composite item from zoho to woocommerce
1181 *
1182 * @param integer $page - Page number of composite item data.
1183 * @param string $category - Category id of composite data.
1184 * @return mixed - mostly array of response message.
1185 */
1186 public function recursively_sync_composite_item_from_zoho( $page, $category ) {
1187 // Start logging.
1188 // $fd = fopen( __DIR__ . '/recursively_sync_composite_item_from_zoho.txt', 'a+' );
1189
1190 global $wpdb;
1191 $zi_org_id = $this->config['ProductZI']['OID'];
1192 $zi_url = $this->config['ProductZI']['APIURL'];
1193
1194 $current_user = wp_get_current_user();
1195 $admin_author_id = $current_user->ID;
1196 if ( ! $admin_author_id ) {
1197 $admin_author_id = 1;
1198 }
1199
1200 $url = $zi_url . 'inventory/v1/compositeitems/?organization_id=' . $zi_org_id . '&filter_by=Status.Active&category_id=' . $category . '&page=' . $page;
1201
1202 $execute_curl_call = new CMBIRD_API_Handler_Zoho();
1203 $json = $execute_curl_call->execute_curl_call_get( $url );
1204 $code = $json->code;
1205 // $message = $json->message;
1206 // fwrite($fd, PHP_EOL . '$json : ' . print_r($json, true));
1207 // Response for item sync with sync button. For cron sync blank array will return.
1208 $response_msg = array();
1209 if ( '0' === $code || 0 === $code ) {
1210 if ( empty( $json->composite_items ) ) {
1211 array_push( $response_msg, $this->zi_response_message( 'ERROR', 'No composite item to sync for category : ' . $category ) );
1212 return $response_msg;
1213 }
1214 // Accounting stock mode check.
1215 $accounting_stock = $this->config['Settings']['enable_accounting_stock'];
1216 foreach ( $json->composite_items as $comp_item ) {
1217 // fwrite( $fd, PHP_EOL . 'Composite Item : ' . print_r( $comp_item, true ) );
1218 // Sync stock from specific location check.
1219 $zi_enable_locationstock = $this->config['Settings']['enable_locationstock'];
1220 $location_id = $this->config['Settings']['location_id'];
1221 $locations = $comp_item->locations;
1222
1223 if ( true === $zi_enable_locationstock ) {
1224 foreach ( $locations as $location ) {
1225 if ( $location->location_id === $location_id ) {
1226 if ( $accounting_stock ) {
1227 $stock = $location->location_available_for_sale_stock;
1228 } else {
1229 $stock = $location->location_actual_available_for_sale_stock;
1230 }
1231 }
1232 }
1233 } elseif ( $accounting_stock ) {
1234 $stock = $comp_item->available_for_sale_stock;
1235 } else {
1236 $stock = $comp_item->actual_available_for_sale_stock;
1237 }
1238
1239 // ----------------- Create composite item in woocommerce--------------.
1240 // Code to skip sync with item already exists with same sku.
1241 $prod_id = wc_get_product_id_by_sku( $comp_item->sku );
1242 // Flag to enable or disable sync.
1243 $allow_to_import = false;
1244 // Check if product exists with same sku.
1245 if ( $prod_id ) {
1246 $zi_item_id = get_post_meta( $prod_id, 'zi_item_id', true );
1247 if ( $zi_item_id === $comp_item->composite_item_id ) {
1248 // If product is with same sku and zi_item_id mapped.
1249 // Do not import ...
1250 $allow_to_import = false;
1251 } else {
1252 // Map existing item with zoho id.
1253 update_post_meta( $prod_id, 'zi_item_id', $comp_item->composite_item_id );
1254 $allow_to_import = false;
1255 }
1256 } else {
1257 // If product not exists normal bahaviour of item sync.
1258 $allow_to_import = true;
1259 }
1260 $zoho_comp_item_id = $comp_item->composite_item_id;
1261 if ( $comp_item->composite_item_id ) {
1262 $child_items = $this->zi_check_if_child_synced_already( $zoho_comp_item_id, $zi_url, $zi_org_id, $prod_id );
1263 // Check if child items already synced with zoho.
1264 if ( ! $child_items ) {
1265 array_push( $response_msg, $this->zi_response_message( 'ERROR', 'Child not synced for composite item : ' . $zoho_comp_item_id ) );
1266 continue;
1267 }
1268 $product_res = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->postmeta} WHERE meta_key = 'zi_item_id' AND meta_value = %s", $zoho_comp_item_id ) );
1269 if ( ! empty( $product_res->post_id ) ) {
1270 $com_prod_id = $product_res->post_id;
1271 }
1272 // Check if item is allowed to import or not.
1273 if ( $allow_to_import ) {
1274 $product_class = new CMBIRD_Products_ZI_Export();
1275 $item_array = json_decode( wp_json_encode( $comp_item ), true );
1276 $com_prod_id = $product_class->cmbird_zi_product_to_woocommerce( $item_array, $stock, 'composite' );
1277 update_post_meta( $com_prod_id, 'zi_item_id', $zoho_comp_item_id );
1278 }
1279 }
1280 // Map composite items to database.
1281 if ( ! empty( $com_prod_id ) ) {
1282 wp_set_object_terms( $com_prod_id, 'bundle', 'product_type' );
1283 foreach ( $child_items as $child_prod ) {
1284 // Adding product to bundle.
1285 $child_bundle_id = $this->add_bundle_product( $child_prod->prod_id, $com_prod_id );
1286 if ( $child_bundle_id ) {
1287 foreach ( $child_prod->metadata as $bundle_meta_key => $bundle_meta_val ) {
1288 $this->zi_update_bundle_meta( $child_bundle_id, $bundle_meta_key, $bundle_meta_val );
1289 }
1290 }
1291 }
1292 }
1293 // --------------------------------------------------------------------.
1294
1295 $is_synced_flag = false; // loggin purpose only .
1296
1297 $product = wc_get_product( $com_prod_id );
1298 foreach ( $comp_item as $key => $value ) {
1299 if ( 'status' === $key ) {
1300 if ( ! empty( $com_prod_id ) ) {
1301 $status = 'active' === $value ? 'publish' : 'draft';
1302 $product->set_status( $status );
1303 }
1304 }
1305 if ( 'description' === $key ) {
1306 if ( ! empty( $com_prod_id ) && ! empty( $value ) ) {
1307 $product->set_short_description( $value );
1308 }
1309 }
1310 if ( 'name' === $key ) {
1311 if ( ! empty( $com_prod_id ) ) {
1312 $product->set_name( $value );
1313 }
1314 }
1315 if ( 'sku' === $key ) {
1316 if ( ! empty( $com_prod_id ) ) {
1317 $product->set_sku( $value );
1318 }
1319 }
1320 // Check if stock sync allowed by plugin.
1321 if ( 'available_stock' === $key || 'actual_available_stock' === $key ) {
1322 $zi_disable_stock_sync = $this->config['Settings']['disable_stock'];
1323 if ( ! $zi_disable_stock_sync ) {
1324 if ( $stock ) {
1325 if ( ! empty( $com_prod_id ) ) {
1326 // If value is less than 0 default 1.
1327 $stock_quantity = $stock < 0 ? 0 : $stock;
1328 $product->set_manage_stock( true );
1329 $product->set_stock_quantity( $stock_quantity );
1330 if ( $stock_quantity > 0 ) {
1331 $status = 'instock';
1332 } else {
1333 $backorder_status = $product->backorders_allowed();
1334 $status = $backorder_status ? 'onbackorder' : 'outofstock';
1335 }
1336 $product->set_stock_status( $status );
1337 update_post_meta( $com_prod_id, '_wc_pb_bundled_items_stock_status', $status );
1338 }
1339 }
1340 }
1341 }
1342 if ( 'rate' === $key ) {
1343 if ( ! empty( $com_prod_id ) ) {
1344 $sale_price = $product->get_sale_price();
1345 if ( empty( $sale_price ) ) {
1346 $product->set_regular_price( $value );
1347 $product->set_price( $value );
1348 update_post_meta( $com_prod_id, '_wc_pb_base_price', $value );
1349 update_post_meta( $com_prod_id, '_wc_pb_base_regular_price', $value );
1350 update_post_meta( $com_prod_id, '_wc_sw_max_regular_price', $value );
1351 } else {
1352 $product->set_regular_price( $value );
1353 update_post_meta( $com_prod_id, '_wc_pb_base_price', $value );
1354 update_post_meta( $com_prod_id, '_wc_pb_base_regular_price', $value );
1355 update_post_meta( $com_prod_id, '_wc_sw_max_regular_price', $value );
1356 }
1357 }
1358 }
1359 $product->save();
1360
1361 if ( 'image_document_id' === $key ) {
1362 if ( ! empty( $com_prod_id ) && ! empty( $value ) ) {
1363 $image_class = new CMBIRD_Image_ZI();
1364 $image_class->cmbird_zi_get_image( $zoho_comp_item_id, $comp_item->name, $comp_item->image_name, $com_prod_id );
1365 }
1366 }
1367 if ( 'category_name' === $key ) {
1368 if ( ! empty( $com_prod_id ) && $comp_item->category_name != '' ) {
1369 $term = get_term_by( 'name', $comp_item->category_name, 'product_cat' );
1370 $term_id = $term->term_id;
1371 if ( empty( $term_id ) ) {
1372 $term = wp_insert_term(
1373 $comp_item->category_name,
1374 'product_cat',
1375 array(
1376 'parent' => 0,
1377 )
1378 );
1379 $term_id = $term['term_id'];
1380 }
1381 if ( $term_id ) {
1382 $existing_terms = wp_get_object_terms( $com_prod_id, 'product_cat' );
1383 if ( $existing_terms && count( $existing_terms ) > 0 ) {
1384 $is_terms_exist = $this->zi_check_terms_exists( $existing_terms, $term_id );
1385 if ( ! $is_terms_exist ) {
1386 update_post_meta( $com_prod_id, 'zi_category_id', $category );
1387 wp_add_object_terms( $com_prod_id, $term_id, 'product_cat' );
1388 }
1389 } else {
1390 update_post_meta( $com_prod_id, 'zi_category_id', $category );
1391 wp_set_object_terms( $com_prod_id, $term_id, 'product_cat' );
1392 }
1393 }
1394 // Remove "uncategorized" category if assigned.
1395 $uncategorized_term = get_term_by( 'slug', 'uncategorized', 'product_cat' );
1396 if ( $uncategorized_term && has_term( $uncategorized_term->term_id, 'product_cat', $com_prod_id ) ) {
1397 wp_remove_object_terms( $com_prod_id, $uncategorized_term->term_id, 'product_cat' );
1398 }
1399 }
1400 }
1401 }
1402
1403 // sync dimensions and weight.
1404 $item_url = "{$zi_url}inventory/v1/compositeitems/{$zoho_comp_item_id}?organization_id={$zi_org_id}";
1405 $this->zi_item_dimension_weight( $item_url, $com_prod_id, true );
1406
1407 // If item synced append to log : logging purpose only.
1408 if ( $is_synced_flag ) {
1409 array_push( $response_msg, $this->zi_response_message( 'SUCCESS', 'Composite item synced for id : ' . $comp_item->composite_item_id, $com_prod_id ) );
1410 }
1411 }
1412
1413 if ( $json->page_context->has_more_page ) {
1414 ++$page;
1415 $this->recursively_sync_composite_item_from_zoho( $page, $category );
1416 }
1417 } else {
1418 array_push( $response_msg, $this->zi_response_message( $code, $json->message ) );
1419 }
1420 // fclose( $fd ); // End of logging.
1421
1422 return $response_msg;
1423 }
1424
1425 /**
1426 * Function to retrieve item details, update weight and dimensions.
1427 *
1428 * @param string $url - URL to ge details.
1429 * @return mixed return true if data false if error.
1430 */
1431 public function zi_item_dimension_weight( $url, $product_id, $is_composite = false ) {
1432 // $fd = fopen(__DIR__ . '/zi_item_dimension_weight.txt', 'a+');
1433 // Check if item is for syncing purpose.
1434 $execute_curl_call = new CMBIRD_API_Handler_Zoho();
1435 $json = $execute_curl_call->execute_curl_call_get( $url );
1436 $code = $json->code;
1437 $message = $json->message;
1438 if ( 0 === $code || '0' === $code ) {
1439 if ( $is_composite ) {
1440 // fwrite($fd, PHP_EOL . '$json : ' . print_r($json, true));
1441 $details = $json->composite_item->package_details;
1442 } else {
1443 $details = $json->item->package_details;
1444 }
1445 $product = wc_get_product( $product_id );
1446 $product->set_weight( floatval( $details->weight ) );
1447 $product->set_length( floatval( $details->length ) );
1448 $product->set_width( floatval( $details->width ) );
1449 $product->set_height( floatval( $details->height ) );
1450 $product->save();
1451 } else {
1452 false;
1453 }
1454 // fclose($fd);
1455 }
1456
1457 /**
1458 * Create response object based on data.
1459 *
1460 * @param mixed $index_col - Index value error message.
1461 * @param string $message - Response message.
1462 * @return object
1463 */
1464 public function zi_response_message( $index_col, $message, $woo_id = '' ) {
1465 return (object) array(
1466 'resp_id' => $index_col,
1467 'message' => $message,
1468 'woo_prod_id' => $woo_id,
1469 );
1470 }
1471
1472 /**
1473 * Helper Function to check if terms already exists.
1474 */
1475 public function zi_check_terms_exists( $existing_terms, $term_id ) {
1476 foreach ( $existing_terms as $woo_existing_term ) {
1477 if ( $woo_existing_term->term_id === $term_id ) {
1478 return true;
1479 } else {
1480 return false;
1481 }
1482 }
1483 }
1484
1485 /**
1486 * Refactored batch sync method using WooCommerce REST API batch endpoint.
1487 *
1488 * Fetches items from Zoho API, optionally fetches itemdetails for location stock,
1489 * and processes items in batches through the WooCommerce REST API.
1490 *
1491 * @param array $args Array containing 'page' and 'category' parameters.
1492 * @return array|bool Success or failure status.
1493 */
1494 public function sync_items_batch( $args ) {
1495 $args = func_get_args();
1496 $config = $this->config['Settings'];
1497 if ( is_array( $args ) ) {
1498 if ( isset( $args['page'] ) && isset( $args['category'] ) ) {
1499 $page = $args['page'];
1500 $category = $args['category'];
1501 } elseif ( isset( $args[0] ) && isset( $args[1] ) ) {
1502 $page = $args[0];
1503 $category = $args[1];
1504 } elseif ( isset( $args[0] ) && ! isset( $args[1] ) ) {
1505 $page = $args[0]['page'];
1506 $category = $args[0]['category'];
1507 } else {
1508 return;
1509 }
1510 } else {
1511 return;
1512 }
1513
1514 // Fetch items from Zoho items endpoint.
1515 $zoho_inventory_oid = $this->config['ProductZI']['OID'];
1516 $zoho_inventory_url = $this->config['ProductZI']['APIURL'];
1517 $timeout = ini_get( 'max_execution_time' ) ? ini_get( 'max_execution_time' ) : 60;
1518
1519 // Dynamically set per_page based on timeout.
1520 if ( $timeout <= 30 ) {
1521 $per_page = 20;
1522 } elseif ( $timeout <= 60 ) {
1523 $per_page = 50;
1524 } else {
1525 $per_page = 100;
1526 }
1527
1528 $url = sprintf(
1529 '%sinventory/v1/items?organization_id=%s&category_id=%s&page=%d&per_page=%d&sort_column=last_modified_time',
1530 $zoho_inventory_url,
1531 $zoho_inventory_oid,
1532 $category,
1533 $page,
1534 $per_page
1535 );
1536 $execute_curl_call = new CMBIRD_API_Handler_Zoho();
1537 $json = $execute_curl_call->execute_curl_call_get( $url );
1538 $code = (int) property_exists( $json, 'code' ) ? $json->code : '0';
1539
1540 if ( 0 !== $code && '0' !== $code ) {
1541 return false;
1542 }
1543
1544 if ( ! isset( $json->items ) || empty( $json->items ) ) {
1545 return false;
1546 }
1547
1548 // Separate items based on location stock requirement.
1549 $items_ready = array();
1550 $item_ids = array();
1551
1552 foreach ( $json->items as $item ) {
1553 // Skip combo products.
1554 if ( isset( $item->is_combo_product ) && $item->is_combo_product ) {
1555 continue;
1556 }
1557
1558 // Collect item IDs if location stock is enabled.
1559 if ( $config['enable_location_stock'] ) {
1560 $item_ids[] = $item->item_id;
1561 } else {
1562 $items_ready[] = $item;
1563 }
1564 }
1565
1566 // Fetch itemdetails for location stock if needed.
1567 if ( ! empty( $item_ids ) ) {
1568 $item_details_url_base = "{$zoho_inventory_url}inventory/v1/itemdetails?organization_id={$zoho_inventory_oid}&item_ids=";
1569 $item_id_str = implode( ',', $item_ids );
1570 $item_details_url = $item_details_url_base . $item_id_str;
1571 $json_details = $execute_curl_call->execute_curl_call_get( $item_details_url );
1572 $details_code = (int) property_exists( $json_details, 'code' ) ? $json_details->code : '0';
1573
1574 if ( 0 === $details_code || '0' === $details_code ) {
1575 if ( isset( $json_details->items ) && is_array( $json_details->items ) ) {
1576 foreach ( $json_details->items as $item_detail ) {
1577 $items_ready[] = $item_detail;
1578 }
1579 }
1580 }
1581 }
1582
1583 // Process items through batch endpoint.
1584 if ( ! empty( $items_ready ) ) {
1585 $this->process_items_batch( $items_ready, $config );
1586 }
1587
1588 // Handle pagination.
1589 if ( isset( $json->page_context->has_more_page ) && $json->page_context->has_more_page && $page < 1000 ) {
1590 as_schedule_single_action(
1591 time() + 5,
1592 'import_simple_items_cron',
1593 array(
1594 'page' => $page + 1,
1595 'category' => $category,
1596 ),
1597 'commercebird'
1598 );
1599 }
1600
1601 return array(
1602 'success' => true,
1603 'message' => 'Batch processed successfully',
1604 );
1605 }
1606
1607 /**
1608 * Process items batch for create/update via WooCommerce REST API.
1609 *
1610 * Handles product matching (by SKU or zi_item_id), duplicate removal,
1611 * and builds create/update arrays for batch endpoint.
1612 *
1613 * @param array $items Array of Zoho items to process.
1614 * @param array $config Configuration array with sync settings.
1615 * @return void
1616 */
1617 private function process_items_batch( $items, $config ) {
1618 global $wpdb;
1619
1620 $to_create = array();
1621 $to_update = array();
1622
1623 foreach ( $items as $item ) {
1624 // Skip variations/groups.
1625 if ( isset( $item->group_id ) && ! empty( $item->group_id ) ) {
1626 $this->sync_variation_of_group( $item );
1627 continue;
1628 }
1629
1630 // 1. Try to find product by SKU.
1631 $product_id = ( isset( $item->sku ) && ! empty( $item->sku ) )
1632 ? wc_get_product_id_by_sku( $item->sku )
1633 : 0;
1634
1635 // 2. If product exists by SKU, check if mapped to Zoho.
1636 if ( $product_id ) {
1637 $zi_item_id = get_post_meta( $product_id, 'zi_item_id', true );
1638 if ( empty( $zi_item_id ) ) {
1639 // Map existing product with Zoho item ID.
1640 update_post_meta( $product_id, 'zi_item_id', $item->item_id );
1641 }
1642 }
1643
1644 // 3. Remove duplicates based on SKU.
1645 if ( $product_id && ! empty( $item->sku ) ) {
1646 $duplicate_product = $wpdb->get_row(
1647 $wpdb->prepare(
1648 "SELECT post_id FROM {$wpdb->prefix}postmeta WHERE meta_key = '_sku' AND meta_value = %s AND post_id != %d LIMIT 1",
1649 $item->sku,
1650 $product_id
1651 )
1652 );
1653 if ( $duplicate_product ) {
1654 wp_delete_post( $duplicate_product->post_id, true );
1655 $product_id = 0;
1656 }
1657 }
1658
1659 // 4. If no product by SKU, try to find by zi_item_id.
1660 if ( empty( $product_id ) ) {
1661 $existing = $wpdb->get_row(
1662 $wpdb->prepare(
1663 "SELECT post_id FROM {$wpdb->prefix}postmeta WHERE meta_key = 'zi_item_id' AND meta_value = %s LIMIT 1",
1664 $item->item_id
1665 )
1666 );
1667 $product_id = $existing ? $existing->post_id : 0;
1668 }
1669
1670 // 5. Create or update based on what we found.
1671 if ( empty( $product_id ) && isset( $item->status ) && 'active' === $item->status ) {
1672 // Create new product.
1673 $to_create[] = $this->map_item_to_array( $item, $config, null );
1674 } elseif ( ! empty( $product_id ) ) {
1675 // Update existing product.
1676 $to_update[] = $this->map_item_to_array( $item, $config, $product_id );
1677 }
1678 }
1679
1680 // Use batch endpoint.
1681 if ( ! empty( $to_create ) ) {
1682 \CommerceBird\Admin\Actions\Sync\ZohoInventorySync::import( 'product', $to_create, false, '/wc/v3/products/batch' );
1683 }
1684
1685 if ( ! empty( $to_update ) ) {
1686 \CommerceBird\Admin\Actions\Sync\ZohoInventorySync::import( 'product', $to_update, true, '/wc/v3/products/batch' );
1687 }
1688 }
1689
1690 /**
1691 * Map Zoho item to WooCommerce batch endpoint format.
1692 *
1693 * Transforms Zoho item data into WooCommerce REST API product format
1694 * for batch create/update operations.
1695 *
1696 * @param object $item Zoho item object.
1697 * @param array $config Configuration array with sync settings.
1698 * @param int $product_id WooCommerce product ID (null for create).
1699 * @return array WooCommerce product data for batch endpoint.
1700 */
1701 private function map_item_to_array( $item, $config, $product_id = null ) {
1702 $data = array(
1703 'name' => isset( $item->name ) ? $item->name : '',
1704 'sku' => isset( $item->sku ) ? $item->sku : '',
1705 'status' => ( isset( $item->status ) && 'active' === $item->status ) ? 'publish' : 'draft',
1706 'regular_price' => isset( $item->rate ) ? (string) $item->rate : '0',
1707 );
1708
1709 // Add product ID for updates.
1710 if ( $product_id ) {
1711 $data['id'] = $product_id;
1712 }
1713
1714 // Add description if not disabled.
1715 if ( ! $config['disable_description'] && isset( $item->description ) ) {
1716 $data['short_description'] = $item->description;
1717 }
1718
1719 // Add images if not disabled.
1720 if ( ! $config['disable_image'] && isset( $item->image_document_id ) && ! empty( $item->image_document_id ) ) {
1721 $image_class = new CMBIRD_Image_ZI();
1722 $attachment_id = $image_class->cmbird_zi_get_image(
1723 $item->item_id,
1724 isset( $item->name ) ? $item->name : '',
1725 isset( $item->image_name ) ? $item->image_name : '',
1726 $product_id
1727 );
1728
1729 // Add image to batch data.
1730 if ( $attachment_id ) {
1731 $data['images'] = array(
1732 array( 'id' => $attachment_id ),
1733 );
1734 }
1735 }
1736
1737 // Check if Tax is enabled and set tax status.
1738 if ( ! empty( $item->tax_id ) && $this->is_tax_enabled ) {
1739 $zi_common_class = new CMBIRD_Common_Functions();
1740 $woo_tax_class = $zi_common_class->get_tax_class_by_percentage( $item->tax_percentage );
1741 $data['tax_class'] = $woo_tax_class;
1742 $data['tax_status'] = 'taxable';
1743 }
1744
1745 // Handle stock.
1746 if ( ! $config['disable_stock'] ) {
1747 $stock = null;
1748
1749 // Use location stock if available (from itemdetails).
1750 if ( isset( $item->locations ) && is_array( $item->locations ) ) {
1751 foreach ( $item->locations as $location ) {
1752 if ( $location->location_id === $config['zoho_location_id'] ) {
1753 $stock = $config['enable_accounting_stock']
1754 ? $location->location_available_for_sale_stock
1755 : $location->location_actual_available_for_sale_stock;
1756 break;
1757 }
1758 }
1759 } else {
1760 // Use global stock from items endpoint.
1761 $stock = $config['enable_accounting_stock']
1762 ? ( isset( $item->available_for_sale_stock ) ? $item->available_for_sale_stock : null )
1763 : ( isset( $item->actual_available_for_sale_stock ) ? $item->actual_available_for_sale_stock : null );
1764 }
1765 if ( null !== $stock ) {
1766 $data['manage_stock'] = true;
1767 $data['stock_quantity'] = $stock;
1768 }
1769 }
1770 // Add cost_of_goods_sold if available.
1771 if ( isset( $item->purchase_rate ) ) {
1772 $data['cost_of_goods_sold'] = $item->purchase_rate;
1773 }
1774
1775 // Add dimensions directly from items endpoint.
1776 if ( isset( $item->weight ) ) {
1777 $data['weight'] = $item->weight;
1778 }
1779 if ( isset( $item->length ) ) {
1780 $data['length'] = $item->length;
1781 }
1782 if ( isset( $item->width ) ) {
1783 $data['width'] = $item->width;
1784 }
1785 if ( isset( $item->height ) ) {
1786 $data['height'] = $item->height;
1787 }
1788
1789 // Add categories.
1790 if ( isset( $item->category_name ) && ! empty( $item->category_name ) ) {
1791 $categories = self::prepare_categories( $item->category_name );
1792 if ( ! empty( $categories ) ) {
1793 $data['categories'] = $categories;
1794 }
1795 }
1796
1797 // Add brand as category if present.
1798 if ( isset( $item->brand ) && ! empty( $item->brand ) ) {
1799 $brand_terms = self::prepare_categories( $item->brand, 'product_brand' );
1800 if ( ! empty( $brand_terms ) ) {
1801 $data['brands'] = $brand_terms;
1802 }
1803 }
1804
1805 // Add meta data.
1806 $data['meta_data'] = array(
1807 array(
1808 'key' => 'zi_item_id',
1809 'value' => isset( $item->item_id ) ? $item->item_id : '',
1810 ),
1811 array(
1812 'key' => 'zi_category_id',
1813 'value' => isset( $item->category_id ) ? $item->category_id : '',
1814 ),
1815 );
1816
1817 // Add custom fields from items endpoint (flattened format).
1818 foreach ( $item as $key => $value ) {
1819 if ( 0 === strpos( $key, 'cf_' ) && false === strpos( $key, '_unformatted' ) ) {
1820 $data['meta_data'][] = array(
1821 'key' => 'zi_' . $key,
1822 'value' => $value,
1823 );
1824 }
1825 }
1826
1827 return $data;
1828 }
1829
1830 /**
1831 * Prepare category data for WooCommerce batch endpoint.
1832 *
1833 * Creates or retrieves category term and returns in batch format.
1834 *
1835 * @param string $category_name Category name from Zoho.
1836 * @param string $taxonomy Taxonomy to use, default is 'product_cat'.
1837 * @return array Category array for WooCommerce batch endpoint.
1838 */
1839 private static function prepare_categories( $category_name, $taxonomy = 'product_cat' ) {
1840 if ( empty( $category_name ) ) {
1841 return array();
1842 }
1843
1844 $term = get_term_by( 'name', $category_name, $taxonomy );
1845 $term_id = $term ? $term->term_id : 0;
1846
1847 if ( ! $term_id ) {
1848 $term = wp_insert_term( $category_name, $taxonomy, array( 'parent' => 0 ) );
1849 $term_id = isset( $term['term_id'] ) ? $term['term_id'] : 0;
1850 }
1851
1852 return $term_id ? array( array( 'id' => $term_id ) ) : array();
1853 }
1854
1855 /**
1856 * Get sync configuration from class config or WordPress options.
1857 *
1858 * Merges class configuration with WordPress option settings.
1859 *
1860 * @return array Configuration array.
1861 */
1862 }
1863 $cmbird_products_zi = new CMBIRD_Products_ZI();
1864