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