PluginProbe ʕ •ᴥ•ʔ
CommerceBird – AI Command Center, ERP Integrations & B2B for WooCommerce (Zoho, Exact Online). / 2.7.5
CommerceBird – AI Command Center, ERP Integrations & B2B for WooCommerce (Zoho, Exact Online). v2.7.5
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 4 months ago class-import-price-list.php 9 months ago class-multi-currency.php 7 months ago class-order-sync.php 4 months ago class-product.php 4 months ago class-users-contact.php 4 months ago index.php 1 year ago
class-import-items.php
1899 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 $variation_sale_price = $variation->get_sale_price();
851 if ( empty( $variation_sale_price ) && ! $zi_disable_price_sync ) {
852 $variation->set_sale_price( $item->rate );
853 }
854 $variation->set_regular_price( $item->rate );
855 // Set Tax Class.
856 if ( $item->tax_id && $is_tax_enabled ) {
857 $zi_common_class = new CMBIRD_Common_Functions();
858 $woo_tax_class = $zi_common_class->get_tax_class_by_percentage( $item->tax_percentage );
859 $variation->set_tax_status( 'taxable' );
860 $variation->set_tax_class( $woo_tax_class );
861 }
862 // Stock Imported code.
863 if ( ! $zi_disable_stock_sync && is_numeric( $stock_quantity ) ) {
864 // fwrite( $fd, PHP_EOL . 'Stock Quantity : ' . $stock_quantity );
865 $variation->set_manage_stock( true );
866 if ( $stock_quantity > 0 ) {
867 $variation->set_manage_stock( true );
868 $variation->set_stock_quantity( $stock_quantity );
869 $variation->set_stock_status( 'instock' );
870 } elseif ( $stock_quantity <= 0 ) {
871 $variation->set_manage_stock( true );
872 $variation->set_stock_quantity( $stock_quantity );
873 $stock_status = $variation->backorders_allowed() ? 'onbackorder' : 'outofstock';
874 $variation->set_stock_status( $stock_status );
875 }
876 }
877 // Update variation attributes if missing or need updating.
878 $attribute_name1 = $item->attribute_option_name1;
879 $attribute_name2 = $item->attribute_option_name2;
880 $attribute_name3 = $item->attribute_option_name3;
881
882 // Prepare the variation data.
883 $attribute_arr = array();
884 if ( ! empty( $attribute_name1 ) ) {
885 $sanitized_name1 = wc_sanitize_taxonomy_name( $item->attribute_name1 );
886 $attribute_arr[ $sanitized_name1 ] = $attribute_name1;
887 }
888 if ( ! empty( $attribute_name2 ) ) {
889 $sanitized_name2 = wc_sanitize_taxonomy_name( $item->attribute_name2 );
890 $attribute_arr[ $sanitized_name2 ] = $attribute_name2;
891 }
892 if ( ! empty( $attribute_name3 ) ) {
893 $sanitized_name3 = wc_sanitize_taxonomy_name( $item->attribute_name3 );
894 $attribute_arr[ $sanitized_name3 ] = $attribute_name3;
895 }
896
897 // Update attributes for the variation.
898 $variation_attributes = array();
899 foreach ( $attribute_arr as $attribute => $term_name ) {
900 $taxonomy = 'pa_' . $attribute;
901
902 // If taxonomy doesn't exist, create it.
903 if ( ! taxonomy_exists( $taxonomy ) ) {
904 register_taxonomy(
905 $taxonomy,
906 'product_variation',
907 array(
908 'hierarchical' => false,
909 'label' => ucfirst( $attribute ),
910 'query_var' => true,
911 'rewrite' => array( 'slug' => sanitize_title( $attribute ) ),
912 ),
913 );
914 }
915
916 // Check if the term exists and create if needed.
917 if ( ! term_exists( $term_name, $taxonomy ) ) {
918 wp_insert_term( $term_name, $taxonomy );
919 }
920
921 // Get the term slug.
922 $term_object = get_term_by( 'name', $term_name, $taxonomy );
923 $term_slug = $term_object ? $term_object->slug : sanitize_title( $term_name );
924
925 // Get the post terms from the parent variable product.
926 $post_term_names = wp_get_post_terms( $group_id, $taxonomy, array( 'fields' => 'names' ) );
927
928 // Set the term on the parent product if not already set.
929 if ( ! in_array( $term_name, $post_term_names, true ) ) {
930 wp_set_post_terms( $group_id, $term_name, $taxonomy, true );
931 }
932
933 // Build variation attributes array for set_attributes().
934 $variation_attributes[ $taxonomy ] = $term_slug;
935 }
936
937 // Set all attributes at once using WooCommerce 10+ API.
938 if ( ! empty( $variation_attributes ) ) {
939 $variation->set_attributes( $variation_attributes );
940 }
941
942 // Featured Image of variation.
943 if ( ! empty( $item->image_document_id ) && ! $disable_image_sync ) {
944 $image_class = new CMBIRD_Image_ZI();
945 $variation_image_id = $image_class->cmbird_zi_get_image( $item->item_id, $item->name, $item->image_name, $variation_id );
946 if ( ! has_post_thumbnail( $group_id ) ) {
947 if ( $variation_image_id ) {
948 set_post_thumbnail( $group_id, $variation_image_id );
949 }
950 }
951 }
952 // enable or disable based on status from Zoho.
953 $status = ( 'active' === $item->status ) ? 'publish' : 'draft';
954 $variation->set_status( $status );
955 $variation->save();
956 // clear cache.
957 wc_delete_product_transients( $variation_id );
958 } else {
959 // create new variation.
960 // if status is not active then return.
961 if ( 'active' !== $item->status ) {
962 return;
963 }
964
965 $attribute_name1 = $item->attribute_option_name1;
966 $attribute_name2 = $item->attribute_option_name2;
967 $attribute_name3 = $item->attribute_option_name3;
968 // Prepare the variation data.
969 $attribute_arr = array();
970 if ( ! empty( $attribute_name1 ) ) {
971 $sanitized_name1 = wc_sanitize_taxonomy_name( $item->attribute_name1 );
972 $attribute_arr[ $sanitized_name1 ] = $attribute_name1;
973 }
974 if ( ! empty( $attribute_name2 ) ) {
975 $sanitized_name2 = wc_sanitize_taxonomy_name( $item->attribute_name2 );
976 $attribute_arr[ $sanitized_name2 ] = $attribute_name2;
977 }
978 if ( ! empty( $attribute_name3 ) ) {
979 $sanitized_name3 = wc_sanitize_taxonomy_name( $item->attribute_name3 );
980 $attribute_arr[ $sanitized_name3 ] = $attribute_name3;
981 }
982 // fwrite( $fd, PHP_EOL . 'Attributes_arr: ' . print_r( $attribute_arr, true ) );
983
984 // here actually create new variation because sku not found.
985 $variation = new WC_Product_Variation();
986 $variation->set_parent_id( $group_id );
987 $variation->set_status( 'publish' );
988 $variation->set_regular_price( $item->rate );
989 $variation->set_sku( $item->sku );
990 if ( ! $zi_disable_stock_sync ) {
991 $variation->set_stock_quantity( $stock_quantity );
992 $variation->set_manage_stock( true );
993 $variation->set_stock_status( '' );
994 } else {
995 $variation->set_manage_stock( false );
996 }
997 $variation->add_meta_data( 'zi_item_id', $item->item_id );
998 $variation_id = $variation->save();
999
1000 // Get the variation attributes with correct attribute values.
1001 $variation_attributes = array();
1002 foreach ( $attribute_arr as $attribute => $term_name ) {
1003 $taxonomy = 'pa_' . $attribute;
1004
1005 // If taxonomy doesn't exist, create it.
1006 if ( ! taxonomy_exists( $taxonomy ) ) {
1007 register_taxonomy(
1008 $taxonomy,
1009 'product_variation',
1010 array(
1011 'hierarchical' => false,
1012 'label' => ucfirst( $attribute ),
1013 'query_var' => true,
1014 'rewrite' => array( 'slug' => sanitize_title( $attribute ) ),
1015 ),
1016 );
1017 }
1018
1019 // Check if the term exists and create if needed.
1020 if ( ! term_exists( $term_name, $taxonomy ) ) {
1021 wp_insert_term( $term_name, $taxonomy );
1022 }
1023
1024 // Get the term slug.
1025 $term_object = get_term_by( 'name', $term_name, $taxonomy );
1026 $term_slug = $term_object ? $term_object->slug : sanitize_title( $term_name );
1027
1028 // Get the post terms from the parent variable product.
1029 $post_term_names = wp_get_post_terms( $group_id, $taxonomy, array( 'fields' => 'names' ) );
1030
1031 // Set the term on the parent product if not already set.
1032 if ( ! in_array( $term_name, $post_term_names, true ) ) {
1033 wp_set_post_terms( $group_id, $term_name, $taxonomy, true );
1034 }
1035
1036 // Build variation attributes array for set_attributes().
1037 $variation_attributes[ $taxonomy ] = $term_slug;
1038 }
1039
1040 // Set all attributes at once using WooCommerce 10+ API.
1041 if ( ! empty( $variation_attributes ) ) {
1042 $variation->set_attributes( $variation_attributes );
1043 $variation->save();
1044 }
1045
1046 // update purchase price as meta data.
1047 if ( ! empty( $item->purchase_rate ) ) {
1048 $variation->update_meta_data( 'cogs_total_value', $item->purchase_rate );
1049 }
1050 // Stock.
1051 if ( ! empty( $stock ) && ! $zi_disable_stock_sync ) {
1052 update_post_meta( $variation_id, 'manage_stock', true );
1053 if ( $stock > 0 ) {
1054 update_post_meta( $variation_id, '_stock', $stock );
1055 update_post_meta( $variation_id, '_stock_status', 'instock' );
1056 } else {
1057 $backorder_status = get_post_meta( $group_id, '_backorders', true );
1058 update_post_meta( $variation_id, '_stock', $stock );
1059 if ( 'yes' === $backorder_status ) {
1060 update_post_meta( $variation_id, '_stock_status', 'onbackorder' );
1061 } else {
1062 update_post_meta( $variation_id, '_stock_status', 'outofstock' );
1063 }
1064 }
1065 }
1066 // Featured Image of variation.
1067 if ( ! empty( $item->image_document_id ) && ! $disable_image_sync ) {
1068 $image_class = new CMBIRD_Image_ZI();
1069 $variation_image_id = $image_class->cmbird_zi_get_image( $item->item_id, $item->name, $item->image_name, $variation_id );
1070 if ( ! has_post_thumbnail( $group_id ) ) {
1071 if ( $variation_image_id ) {
1072 set_post_thumbnail( $group_id, $variation_image_id );
1073 }
1074 }
1075 }
1076 update_post_meta( $variation_id, 'zi_item_id', $item_id );
1077 // WC_Product_Variable::sync( $group_id );
1078 // Regenerate lookup table for attributes.
1079 $lookup_data_store = new LookupDataStore();
1080 $lookup_data_store->create_data_for_product( $group_id );
1081 // End group item add process.
1082 unset( $attribute_arr );
1083 }
1084 // end of grouped item updating.
1085 }
1086 }
1087
1088 /**
1089 * Helper Function to check if child of composite items already synced or not
1090 *
1091 * @param string $composite_zoho_id - zoho composite item id to check if it's child are already synced.
1092 * @param string $zi_url - zoho api url.
1093 * @param string $zi_org_id - zoho organization token.
1094 * @param string $prod_id - woocommerce product id.
1095 * @return array | bool of child id and metadata if child item already synced else will return false.
1096 */
1097 public function zi_check_if_child_synced_already( $composite_zoho_id, $zi_url, $zi_org_id, $prod_id ) {
1098 if ( $prod_id ) {
1099 $bundle_childs = WC_PB_DB::query_bundled_items(
1100 array(
1101 'return' => 'id=>product_id',
1102 'bundle_id' => array( $prod_id ),
1103 )
1104 );
1105 }
1106 global $wpdb;
1107
1108 $url = $zi_url . 'inventory/v1/compositeitems/' . $composite_zoho_id . '?organization_id=' . $zi_org_id;
1109
1110 $execute_curl_call = new CMBIRD_API_Handler_Zoho();
1111 $json = $execute_curl_call->execute_curl_call_get( $url );
1112 $code = $json->code;
1113 // Flag to allow sync of parent composite item.
1114 $allow_sync = false;
1115 // Array of child object metadata.
1116 $product_array = array(); // [{prod_id:'',metadata:{key:'',value:''}},...].
1117 if ( '0' === $code || 0 === $code ) {
1118 foreach ( $json->composite_item->mapped_items as $child_item ) {
1119 $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 ) );
1120 // If any child will not have zoho id in meta field then process will return false and syncing will be skipped for given item.
1121 if ( ! empty( $prod_meta->post_id ) ) {
1122
1123 $allow_sync = true;
1124 $product = wc_get_product( $prod_meta->post_id );
1125 $stock_status = 'out_of_stock';
1126 if ( $child_item->stock_on_hand > 0 ) {
1127 $stock_status = 'in_stock';
1128 } elseif ( $product && $product->backorders_allowed() ) {
1129 $stock_status = 'onbackorder';
1130 }
1131 $prod_obj = (object) array(
1132 'prod_id' => $prod_meta->post_id,
1133 'metadata' => (object) array(
1134 'quantity_min' => max( 1, $child_item->quantity ),
1135 'quantity_max' => max( 1, $child_item->quantity ),
1136 'stock_status' => $stock_status,
1137 'max_stock' => $child_item->stock_on_hand,
1138 ),
1139 );
1140 if ( is_array( $bundle_childs ) && ! empty( $bundle_childs ) ) {
1141 $index = array_search( $prod_meta->post_id, $bundle_childs );
1142 unset( $bundle_childs[ $index ] );
1143 }
1144 array_push( $product_array, $prod_obj );
1145 } else {
1146 continue;
1147 }
1148 }
1149 }
1150 if ( is_array( $bundle_childs ) && ! empty( $bundle_childs ) ) {
1151 foreach ( $bundle_childs as $item_id => $val ) {
1152 WC_PB_DB::delete_bundled_item( $item_id );
1153 }
1154 }
1155 if ( $allow_sync ) {
1156 return $product_array;
1157 }
1158 return false;
1159 }
1160 /**
1161 * Mapping of bundled product
1162 *
1163 * @param number $product_id - Product id of child item of bundle product.
1164 * @param number $bundle_id - Bundle id of product.
1165 * @param number $menu_order - Listing order of child product ($menu_order will useful at composite product details page).
1166 * @return number - bundle item id.
1167 */
1168 public function add_bundle_product( $product_id, $bundle_id, $menu_order = 0 ) {
1169 $bundle_items = WC_PB_DB::query_bundled_items(
1170 array(
1171 'return' => 'id=>product_id',
1172 'bundle_id' => array( $bundle_id ),
1173 'product_id' => array( $product_id ),
1174 )
1175 );
1176 $data = array(
1177 'menu_order' => $menu_order,
1178 );
1179
1180 if ( count( $bundle_items ) > 0 ) {
1181 $result = WC_PB_DB::update_bundled_item( $bundle_id, $data );
1182 return $result;
1183 } else {
1184 // create data array of bundle item.
1185 $data = array(
1186 'product_id' => $product_id,
1187 'bundle_id' => $bundle_id,
1188 'menu_order' => $menu_order,
1189 );
1190 $bundle_id = WC_PB_DB::add_bundled_item( $data );
1191 return $bundle_id;
1192 }
1193 }
1194
1195 /**
1196 * Create or update bundle item metadata
1197 *
1198 * @param number $bundle_item_id bundle item id.
1199 * @param string $meta_key - metadata key.
1200 * @param string $meta_value - metadata value.
1201 * @return float|int $result - metadata id.
1202 */
1203 public function zi_update_bundle_meta( $bundle_item_id, $meta_key, $meta_value ) {
1204 // first get metadata from db.
1205 $metadata = WC_PB_DB::get_bundled_item_meta( $bundle_item_id, $meta_key );
1206 if ( $metadata ) {
1207 $result = WC_PB_DB::update_bundled_item_meta( $bundle_item_id, $meta_key, $meta_value );
1208 } else {
1209 $result = WC_PB_DB::add_bundled_item_meta( $bundle_item_id, $meta_key, $meta_value );
1210 }
1211 return $result;
1212 }
1213
1214 /**
1215 * Function to sync composite item from zoho to woocommerce
1216 *
1217 * @param integer $page - Page number of composite item data.
1218 * @param string $category - Category id of composite data.
1219 * @return mixed - mostly array of response message.
1220 */
1221 public function recursively_sync_composite_item_from_zoho( $page, $category ) {
1222 // Start logging.
1223 // $fd = fopen( __DIR__ . '/recursively_sync_composite_item_from_zoho.txt', 'a+' );
1224
1225 global $wpdb;
1226 $zi_org_id = $this->config['ProductZI']['OID'];
1227 $zi_url = $this->config['ProductZI']['APIURL'];
1228
1229 $current_user = wp_get_current_user();
1230 $admin_author_id = $current_user->ID;
1231 if ( ! $admin_author_id ) {
1232 $admin_author_id = 1;
1233 }
1234
1235 $url = $zi_url . 'inventory/v1/compositeitems/?organization_id=' . $zi_org_id . '&filter_by=Status.Active&category_id=' . $category . '&page=' . $page;
1236
1237 $execute_curl_call = new CMBIRD_API_Handler_Zoho();
1238 $json = $execute_curl_call->execute_curl_call_get( $url );
1239 $code = $json->code;
1240 // $message = $json->message;
1241 // fwrite($fd, PHP_EOL . '$json : ' . print_r($json, true));
1242 // Response for item sync with sync button. For cron sync blank array will return.
1243 $response_msg = array();
1244 if ( '0' === $code || 0 === $code ) {
1245 if ( empty( $json->composite_items ) ) {
1246 array_push( $response_msg, $this->zi_response_message( 'ERROR', 'No composite item to sync for category : ' . $category ) );
1247 return $response_msg;
1248 }
1249 // Accounting stock mode check.
1250 $accounting_stock = $this->config['Settings']['enable_accounting_stock'];
1251 foreach ( $json->composite_items as $comp_item ) {
1252 // fwrite( $fd, PHP_EOL . 'Composite Item : ' . print_r( $comp_item, true ) );
1253 // Sync stock from specific location check.
1254 $zi_enable_locationstock = $this->config['Settings']['enable_locationstock'];
1255 $location_id = $this->config['Settings']['location_id'];
1256 $locations = $comp_item->locations;
1257
1258 if ( true === $zi_enable_locationstock ) {
1259 foreach ( $locations as $location ) {
1260 if ( $location->location_id === $location_id ) {
1261 if ( $accounting_stock ) {
1262 $stock = $location->location_available_for_sale_stock;
1263 } else {
1264 $stock = $location->location_actual_available_for_sale_stock;
1265 }
1266 }
1267 }
1268 } elseif ( $accounting_stock ) {
1269 $stock = $comp_item->available_for_sale_stock;
1270 } else {
1271 $stock = $comp_item->actual_available_for_sale_stock;
1272 }
1273
1274 // ----------------- Create composite item in woocommerce--------------.
1275 // Code to skip sync with item already exists with same sku.
1276 $prod_id = wc_get_product_id_by_sku( $comp_item->sku );
1277 // Flag to enable or disable sync.
1278 $allow_to_import = false;
1279 // Check if product exists with same sku.
1280 if ( $prod_id ) {
1281 $zi_item_id = get_post_meta( $prod_id, 'zi_item_id', true );
1282 if ( $zi_item_id === $comp_item->composite_item_id ) {
1283 // If product is with same sku and zi_item_id mapped.
1284 // Do not import ...
1285 $allow_to_import = false;
1286 } else {
1287 // Map existing item with zoho id.
1288 update_post_meta( $prod_id, 'zi_item_id', $comp_item->composite_item_id );
1289 $allow_to_import = false;
1290 }
1291 } else {
1292 // If product not exists normal bahaviour of item sync.
1293 $allow_to_import = true;
1294 }
1295 $zoho_comp_item_id = $comp_item->composite_item_id;
1296 if ( $comp_item->composite_item_id ) {
1297 $child_items = $this->zi_check_if_child_synced_already( $zoho_comp_item_id, $zi_url, $zi_org_id, $prod_id );
1298 // Check if child items already synced with zoho.
1299 if ( ! $child_items ) {
1300 array_push( $response_msg, $this->zi_response_message( 'ERROR', 'Child not synced for composite item : ' . $zoho_comp_item_id ) );
1301 continue;
1302 }
1303 $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 ) );
1304 if ( ! empty( $product_res->post_id ) ) {
1305 $com_prod_id = $product_res->post_id;
1306 }
1307 // Check if item is allowed to import or not.
1308 if ( $allow_to_import ) {
1309 $product_class = new CMBIRD_Products_ZI_Export();
1310 $item_array = json_decode( wp_json_encode( $comp_item ), true );
1311 $com_prod_id = $product_class->cmbird_zi_product_to_woocommerce( $item_array, $stock, 'composite' );
1312 update_post_meta( $com_prod_id, 'zi_item_id', $zoho_comp_item_id );
1313 }
1314 }
1315 // Map composite items to database.
1316 if ( ! empty( $com_prod_id ) ) {
1317 wp_set_object_terms( $com_prod_id, 'bundle', 'product_type' );
1318 foreach ( $child_items as $child_prod ) {
1319 // Adding product to bundle.
1320 $child_bundle_id = $this->add_bundle_product( $child_prod->prod_id, $com_prod_id );
1321 if ( $child_bundle_id ) {
1322 foreach ( $child_prod->metadata as $bundle_meta_key => $bundle_meta_val ) {
1323 $this->zi_update_bundle_meta( $child_bundle_id, $bundle_meta_key, $bundle_meta_val );
1324 }
1325 }
1326 }
1327 }
1328 // --------------------------------------------------------------------.
1329
1330 $is_synced_flag = false; // loggin purpose only .
1331
1332 $product = wc_get_product( $com_prod_id );
1333 foreach ( $comp_item as $key => $value ) {
1334 if ( 'status' === $key ) {
1335 if ( ! empty( $com_prod_id ) ) {
1336 $status = 'active' === $value ? 'publish' : 'draft';
1337 $product->set_status( $status );
1338 }
1339 }
1340 if ( 'description' === $key ) {
1341 if ( ! empty( $com_prod_id ) && ! empty( $value ) ) {
1342 $product->set_short_description( $value );
1343 }
1344 }
1345 if ( 'name' === $key ) {
1346 if ( ! empty( $com_prod_id ) ) {
1347 $product->set_name( $value );
1348 }
1349 }
1350 if ( 'sku' === $key ) {
1351 if ( ! empty( $com_prod_id ) ) {
1352 $product->set_sku( $value );
1353 }
1354 }
1355 // Check if stock sync allowed by plugin.
1356 if ( 'available_stock' === $key || 'actual_available_stock' === $key ) {
1357 $zi_disable_stock_sync = $this->config['Settings']['disable_stock'];
1358 if ( ! $zi_disable_stock_sync ) {
1359 if ( $stock ) {
1360 if ( ! empty( $com_prod_id ) ) {
1361 // If value is less than 0 default 1.
1362 $stock_quantity = $stock < 0 ? 0 : $stock;
1363 $product->set_manage_stock( true );
1364 $product->set_stock_quantity( $stock_quantity );
1365 if ( $stock_quantity > 0 ) {
1366 $status = 'instock';
1367 } else {
1368 $backorder_status = $product->backorders_allowed();
1369 $status = $backorder_status ? 'onbackorder' : 'outofstock';
1370 }
1371 $product->set_stock_status( $status );
1372 update_post_meta( $com_prod_id, '_wc_pb_bundled_items_stock_status', $status );
1373 }
1374 }
1375 }
1376 }
1377 if ( 'rate' === $key ) {
1378 if ( ! empty( $com_prod_id ) ) {
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, $config );
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 * @param array $config Configuration array with sync settings.
1656 * @return void
1657 */
1658 private function process_items_batch( $items, $config ) {
1659 global $wpdb;
1660
1661 $to_create = array();
1662 $to_update = array();
1663
1664 foreach ( $items as $item ) {
1665 // 1. Try to find product by SKU.
1666 $product_id = ( isset( $item->sku ) && ! empty( $item->sku ) )
1667 ? wc_get_product_id_by_sku( $item->sku )
1668 : 0;
1669
1670 // 2. If product exists by SKU, check if mapped to Zoho.
1671 if ( $product_id ) {
1672 $zi_item_id = get_post_meta( $product_id, 'zi_item_id', true );
1673 if ( empty( $zi_item_id ) ) {
1674 // Map existing product with Zoho item ID.
1675 update_post_meta( $product_id, 'zi_item_id', $item->item_id );
1676 }
1677 }
1678
1679 // 3. Remove duplicates based on SKU.
1680 if ( $product_id && ! empty( $item->sku ) ) {
1681 $duplicate_product = $wpdb->get_row(
1682 $wpdb->prepare(
1683 "SELECT post_id FROM {$wpdb->prefix}postmeta WHERE meta_key = '_sku' AND meta_value = %s AND post_id != %d LIMIT 1",
1684 $item->sku,
1685 $product_id
1686 )
1687 );
1688 if ( $duplicate_product ) {
1689 wp_delete_post( $duplicate_product->post_id, true );
1690 $product_id = 0;
1691 }
1692 }
1693
1694 // 4. If no product by SKU, try to find by zi_item_id.
1695 if ( empty( $product_id ) ) {
1696 $existing = $wpdb->get_row(
1697 $wpdb->prepare(
1698 "SELECT post_id FROM {$wpdb->prefix}postmeta WHERE meta_key = 'zi_item_id' AND meta_value = %s LIMIT 1",
1699 $item->item_id
1700 )
1701 );
1702 $product_id = $existing ? $existing->post_id : 0;
1703 }
1704
1705 // 5. Create or update based on what we found.
1706 if ( empty( $product_id ) && isset( $item->status ) && 'active' === $item->status ) {
1707 // Create new product.
1708 $to_create[] = $this->map_item_to_array( $item, $config, null );
1709 } elseif ( ! empty( $product_id ) ) {
1710 // Update existing product.
1711 $to_update[] = $this->map_item_to_array( $item, $config, $product_id );
1712 }
1713 }
1714
1715 // Use batch endpoint.
1716 if ( ! empty( $to_create ) ) {
1717 \CommerceBird\Admin\Actions\Sync\ZohoInventorySync::import( 'product', $to_create, false, '/wc/v3/products/batch' );
1718 }
1719
1720 if ( ! empty( $to_update ) ) {
1721 \CommerceBird\Admin\Actions\Sync\ZohoInventorySync::import( 'product', $to_update, true, '/wc/v3/products/batch' );
1722 }
1723 }
1724
1725 /**
1726 * Map Zoho item to WooCommerce batch endpoint format.
1727 *
1728 * Transforms Zoho item data into WooCommerce REST API product format
1729 * for batch create/update operations.
1730 *
1731 * @param object $item Zoho item object.
1732 * @param array $config Configuration array with sync settings.
1733 * @param int $product_id WooCommerce product ID (null for create).
1734 * @return array WooCommerce product data for batch endpoint.
1735 */
1736 private function map_item_to_array( $item, $config, $product_id = null ) {
1737 $data = array(
1738 'name' => isset( $item->name ) ? $item->name : '',
1739 'sku' => isset( $item->sku ) ? $item->sku : '',
1740 'status' => ( isset( $item->status ) && 'active' === $item->status ) ? 'publish' : 'draft',
1741 'regular_price' => isset( $item->rate ) ? (string) $item->rate : '0',
1742 );
1743
1744 // Add product ID for updates.
1745 if ( $product_id ) {
1746 $data['id'] = $product_id;
1747 }
1748
1749 // Add description if not disabled.
1750 if ( ! $config['disable_description'] && isset( $item->description ) ) {
1751 $data['short_description'] = $item->description;
1752 }
1753
1754 // Add images if not disabled.
1755 if ( ! $config['disable_image'] && isset( $item->image_document_id ) && ! empty( $item->image_document_id ) ) {
1756 $image_class = new CMBIRD_Image_ZI();
1757 $attachment_id = $image_class->cmbird_zi_get_image(
1758 $item->item_id,
1759 isset( $item->name ) ? $item->name : '',
1760 isset( $item->image_name ) ? $item->image_name : '',
1761 $product_id
1762 );
1763
1764 // Add image to batch data.
1765 if ( $attachment_id ) {
1766 $data['images'] = array(
1767 array( 'id' => $attachment_id ),
1768 );
1769 }
1770 }
1771
1772 // Check if Tax is enabled and set tax status.
1773 if ( ! empty( $item->tax_id ) && $this->is_tax_enabled ) {
1774 $zi_common_class = new CMBIRD_Common_Functions();
1775 $woo_tax_class = $zi_common_class->get_tax_class_by_percentage( $item->tax_percentage );
1776 $data['tax_class'] = $woo_tax_class;
1777 $data['tax_status'] = 'taxable';
1778 }
1779
1780 // Handle stock.
1781 if ( ! $config['disable_stock'] ) {
1782 $stock = null;
1783
1784 // Use location stock if available (from itemdetails).
1785 if ( isset( $item->locations ) && is_array( $item->locations ) ) {
1786 foreach ( $item->locations as $location ) {
1787 if ( $location->location_id === $config['zoho_location_id'] ) {
1788 $stock = $config['enable_accounting_stock']
1789 ? $location->location_available_for_sale_stock
1790 : $location->location_actual_available_for_sale_stock;
1791 break;
1792 }
1793 }
1794 } else {
1795 // Use global stock from items endpoint.
1796 $stock = $config['enable_accounting_stock']
1797 ? ( isset( $item->available_for_sale_stock ) ? $item->available_for_sale_stock : null )
1798 : ( isset( $item->actual_available_for_sale_stock ) ? $item->actual_available_for_sale_stock : null );
1799 }
1800 if ( null !== $stock ) {
1801 $data['manage_stock'] = true;
1802 $data['stock_quantity'] = $stock;
1803 }
1804 }
1805 // Add cost_of_goods_sold if available.
1806 if ( isset( $item->purchase_rate ) ) {
1807 $data['cost_of_goods_sold'] = $item->purchase_rate;
1808 }
1809
1810 // Add dimensions directly from items endpoint.
1811 if ( isset( $item->weight ) ) {
1812 $data['weight'] = $item->weight;
1813 }
1814 if ( isset( $item->length ) ) {
1815 $data['length'] = $item->length;
1816 }
1817 if ( isset( $item->width ) ) {
1818 $data['width'] = $item->width;
1819 }
1820 if ( isset( $item->height ) ) {
1821 $data['height'] = $item->height;
1822 }
1823
1824 // Add categories.
1825 if ( isset( $item->category_name ) && ! empty( $item->category_name ) ) {
1826 $categories = self::prepare_categories( $item->category_name );
1827 if ( ! empty( $categories ) ) {
1828 $data['categories'] = $categories;
1829 }
1830 }
1831
1832 // Add brand as category if present.
1833 if ( isset( $item->brand ) && ! empty( $item->brand ) ) {
1834 $brand_terms = self::prepare_categories( $item->brand, 'product_brand' );
1835 if ( ! empty( $brand_terms ) ) {
1836 $data['brands'] = $brand_terms;
1837 }
1838 }
1839
1840 // Add meta data.
1841 $data['meta_data'] = array(
1842 array(
1843 'key' => 'zi_item_id',
1844 'value' => isset( $item->item_id ) ? $item->item_id : '',
1845 ),
1846 array(
1847 'key' => 'zi_category_id',
1848 'value' => isset( $item->category_id ) ? $item->category_id : '',
1849 ),
1850 );
1851
1852 // Add custom fields from items endpoint (flattened format).
1853 foreach ( $item as $key => $value ) {
1854 if ( 0 === strpos( $key, 'cf_' ) && false === strpos( $key, '_unformatted' ) ) {
1855 $data['meta_data'][] = array(
1856 'key' => 'zi_' . $key,
1857 'value' => $value,
1858 );
1859 }
1860 }
1861
1862 return $data;
1863 }
1864
1865 /**
1866 * Prepare category data for WooCommerce batch endpoint.
1867 *
1868 * Creates or retrieves category term and returns in batch format.
1869 *
1870 * @param string $category_name Category name from Zoho.
1871 * @param string $taxonomy Taxonomy to use, default is 'product_cat'.
1872 * @return array Category array for WooCommerce batch endpoint.
1873 */
1874 private static function prepare_categories( $category_name, $taxonomy = 'product_cat' ) {
1875 if ( empty( $category_name ) ) {
1876 return array();
1877 }
1878
1879 $term = get_term_by( 'name', $category_name, $taxonomy );
1880 $term_id = $term ? $term->term_id : 0;
1881
1882 if ( ! $term_id ) {
1883 $term = wp_insert_term( $category_name, $taxonomy, array( 'parent' => 0 ) );
1884 $term_id = isset( $term['term_id'] ) ? $term['term_id'] : 0;
1885 }
1886
1887 return $term_id ? array( array( 'id' => $term_id ) ) : array();
1888 }
1889
1890 /**
1891 * Get sync configuration from class config or WordPress options.
1892 *
1893 * Merges class configuration with WordPress option settings.
1894 *
1895 * @return array Configuration array.
1896 */
1897 }
1898 $cmbird_products_zi = new CMBIRD_Products_ZI();
1899