PluginProbe ʕ •ᴥ•ʔ
CommerceBird – AI Command Center, ERP Integrations & B2B for WooCommerce (Zoho, Exact Online). / 2.5.1
CommerceBird – AI Command Center, ERP Integrations & B2B for WooCommerce (Zoho, Exact Online). v2.5.1
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-crm / class-zcrm-product.php
commercebird / includes / classes / zoho-crm Last commit date
class-zcrm-contact.php 9 months ago class-zcrm-modules.php 9 months ago class-zcrm-product.php 9 months ago class-zcrm-salesorder.php 9 months ago index.php 1 year ago
class-zcrm-product.php
908 lines
1 <?php
2
3 if ( ! defined( 'ABSPATH' ) ) {
4 exit;
5 }
6
7 class CMBIRD_ZCRM_Product {
8
9 protected $zcrm_modules;
10 protected $zoho_crm_url;
11 protected $zcrm_custom_fields;
12
13 public function __construct() {
14 $this->zcrm_modules = new CMBIRD_ZCRM_Modules();
15 $this->zoho_crm_url = rtrim( get_option( 'cmbird_zoho_crm_url' ), '/' ) . '/';
16 // get the option for custom fields.
17 $this->zcrm_custom_fields = get_option( 'zcrm_products_fields', array() );
18 }
19
20 /**
21 * Log messages using WooCommerce logger if available
22 *
23 * @param string $message The message to log
24 * @param string $level Log level: debug, info, notice, warning, error, critical, alert, emergency
25 */
26 private function log( $message, $level = 'info' ) {
27 if ( function_exists( 'wc_get_logger' ) ) {
28 $logger = wc_get_logger();
29 $logger->log( $level, $message, array( 'source' => 'commercebird' ) );
30 }
31 }
32
33 /**
34 * Main product sync function - handles create/update to avoid duplication
35 * Supports variable products and their variations
36 *
37 * @param WC_Product $product WooCommerce product object
38 * @return string|array|null Zoho product ID(s) on success, null on failure
39 */
40 public function cmbird_sync_product_to_zoho( $product ) {
41 if ( ! $product || ! is_a( $product, 'WC_Product' ) ) {
42 return null;
43 }
44
45 // Handle variable products differently.
46 if ( $product->is_type( 'variable' ) ) {
47 return $this->sync_variable_product_to_zoho( $product );
48 }
49
50 // Handle simple products and variations.
51 return $this->sync_single_product_to_zoho( $product );
52 }
53
54 /**
55 * Sync a single product (simple product or variation) to Zoho
56 *
57 * @param WC_Product $product WooCommerce product object
58 * @return string|null Zoho product ID on success, null on failure
59 */
60 protected function sync_single_product_to_zoho( $product ) {
61 // First, try to get existing Zoho product ID.
62 $zoho_product_id = $this->cmbird_get_zoho_product_id( $product );
63
64 if ( $zoho_product_id ) {
65 // Update existing product.
66 $result = $this->update_zoho_product( $product, $zoho_product_id );
67 return $result ? $zoho_product_id : null;
68 } else {
69 // Create new product.
70 return $this->create_zoho_product( $product );
71 }
72 }
73
74 /**
75 * Sync a variable product and all its variations to Zoho
76 *
77 * @param WC_Product_Variable $product Variable product object
78 * @return array Array of results with parent and variation sync results
79 */
80 protected function sync_variable_product_to_zoho( $product ) {
81 $results = array(
82 'parent_product_id' => null,
83 'variations' => array(),
84 'success_count' => 0,
85 'failed_count' => 0,
86 );
87
88 // First sync the parent variable product.
89 $parent_zoho_id = $this->sync_single_product_to_zoho( $product );
90 $results['parent_product_id'] = $parent_zoho_id;
91
92 if ( $parent_zoho_id ) {
93 ++$results['success_count'];
94 } else {
95 ++$results['failed_count'];
96 }
97
98 // Get all variations.
99 $variation_ids = $product->get_children();
100
101 if ( ! empty( $variation_ids ) ) {
102 foreach ( $variation_ids as $variation_id ) {
103 $variation = wc_get_product( $variation_id );
104
105 if ( ! $variation || ! $variation->is_type( 'variation' ) ) {
106 $results['variations'][ $variation_id ] = array(
107 'status' => 'error',
108 'message' => 'Invalid variation product',
109 'zoho_product_id' => null,
110 'action' => 'skipped',
111 );
112 ++$results['failed_count'];
113 continue;
114 }
115
116 // Check if variation already has Zoho ID (for better reporting).
117 $existing_zoho_id = get_post_meta( $variation_id, 'zcrm_product_id', true );
118 $action = empty( $existing_zoho_id ) ? 'created' : 'updated';
119
120 // Sync the variation (handles duplication check and meta update automatically).
121 $variation_zoho_id = $this->sync_single_product_to_zoho( $variation );
122
123 if ( $variation_zoho_id ) {
124 $results['variations'][ $variation_id ] = array(
125 'status' => 'success',
126 'message' => sprintf( 'Variation %s successfully', $action ),
127 'zoho_product_id' => $variation_zoho_id,
128 'action' => $action,
129 );
130 ++$results['success_count'];
131
132 // Store parent-child relationship in meta (always update this).
133 if ( $parent_zoho_id ) {
134 update_post_meta( $variation_id, 'zcrm_parent_product_id', $parent_zoho_id );
135 }
136 } else {
137 $results['variations'][ $variation_id ] = array(
138 'status' => 'error',
139 'message' => sprintf( 'Failed to %s variation', 'created' === $action ? 'create' : 'update' ),
140 'zoho_product_id' => null,
141 'action' => 'failed',
142 );
143 ++$results['failed_count'];
144 }
145
146 // Add delay to avoid rate limiting.
147 usleep( 200000 ); // 0.2 seconds.
148 }
149 }
150
151 return $results;
152 }
153
154 /**
155 * Get Zoho product ID for a WooCommerce product
156 * First checks local meta, then searches Zoho by SKU, then by name
157 *
158 * @param WC_Product $product WooCommerce product object
159 * @return string|null Zoho product ID if found, null otherwise
160 */
161 public function cmbird_get_zoho_product_id( $product ) {
162 $woo_product_id = $product->get_id();
163 $zcrm_product_id = get_post_meta( $woo_product_id, 'zcrm_product_id', true );
164
165 if ( ! empty( $zcrm_product_id ) ) {
166 // Verify the product still exists in Zoho.
167 if ( $this->verify_zoho_product_exists( $zcrm_product_id ) ) {
168 return $zcrm_product_id;
169 } else {
170 // Clean up invalid meta.
171 delete_post_meta( $woo_product_id, 'zcrm_product_id' );
172 }
173 }
174
175 // Try to find product by SKU first (more reliable).
176 if ( $product->get_sku() ) {
177 $zcrm_product_id = $this->find_zoho_product_by_sku( $product->get_sku() );
178 if ( $zcrm_product_id ) {
179 // Save to WooCommerce meta for future lookups.
180 update_post_meta( $woo_product_id, 'zcrm_product_id', $zcrm_product_id );
181 return $zcrm_product_id;
182 }
183 }
184
185 // Fallback: Try to find product by name.
186 $zcrm_product_id = $this->find_zoho_product_by_name( $product->get_name() );
187 if ( $zcrm_product_id ) {
188 // Save to WooCommerce meta for future lookups.
189 update_post_meta( $woo_product_id, 'zcrm_product_id', $zcrm_product_id );
190 return $zcrm_product_id;
191 }
192
193 return null;
194 }
195
196 /**
197 * Get Zoho product by ID
198 *
199 * @param string $zoho_product_id Zoho product ID
200 * @return object|WP_Error Zoho product data or error
201 */
202 public function cmbird_get_zoho_product_by_id( $zoho_product_id ) {
203 if ( empty( $zoho_product_id ) ) {
204 return new WP_Error( 'invalid_id', 'Product ID is required' );
205 }
206
207 $response = $this->zcrm_modules->cmbird_get_record( 'Products', $zoho_product_id );
208
209 if ( is_wp_error( $response ) ) {
210 return $response;
211 }
212
213 if ( empty( $response->data[0] ) ) {
214 return new WP_Error( 'product_not_found', 'Product not found in Zoho CRM' );
215 }
216
217 return $response->data[0];
218 }
219
220 /**
221 * Find Zoho product by SKU (Product_Code)
222 *
223 * @param string $sku Product SKU
224 * @return string|null Zoho product ID if found, null otherwise
225 */
226 protected function find_zoho_product_by_sku( $sku ) {
227 if ( empty( $sku ) ) {
228 return null;
229 }
230
231 $search_url = $this->zoho_crm_url . 'crm/v7/Products/search?criteria=Product_Code:equals:' . rawurlencode( $sku );
232 $api = new CMBIRD_API_Handler_Zoho();
233 $response = $api->execute_curl_call_get( $search_url );
234
235 if ( is_wp_error( $response ) || empty( $response->data[0]->id ) ) {
236 return null;
237 }
238
239 return $response->data[0]->id;
240 }
241
242 /**
243 * Find Zoho product by name
244 *
245 * @param string $name Product name
246 * @return string|null Zoho product ID if found, null otherwise
247 */
248 protected function find_zoho_product_by_name( $name ) {
249 if ( empty( $name ) ) {
250 return null;
251 }
252
253 $search_url = $this->zoho_crm_url . 'crm/v7/Products/search?word=' . rawurlencode( $name );
254 $api = new CMBIRD_API_Handler_Zoho();
255 $response = $api->execute_curl_call_get( $search_url );
256
257 if ( is_wp_error( $response ) || empty( $response->data[0]->id ) ) {
258 return null;
259 }
260
261 return $response->data[0]->id;
262 }
263
264 /**
265 * Verify that a Zoho product exists
266 *
267 * @param string $zoho_product_id Zoho product ID
268 * @return bool True if product exists, false otherwise
269 */
270 protected function verify_zoho_product_exists( $zoho_product_id ) {
271 $response = $this->cmbird_get_zoho_product_by_id( $zoho_product_id );
272 return ! is_wp_error( $response );
273 }
274
275 /**
276 * Create a new product in Zoho CRM
277 *
278 * @param WC_Product $product WooCommerce product object
279 * @return string|null Zoho product ID on success, null on failure
280 */
281 protected function create_zoho_product( $product ) {
282 $product_payload = $this->format_product_for_zoho( $product );
283
284 $response = $this->zcrm_modules->cmbird_create_record( 'Products', $product_payload );
285
286 if ( is_wp_error( $response ) || empty( $response->data[0]->details->id ) ) {
287 // Log error for debugging but remove for production.
288 return null;
289 }
290
291 $zoho_product_id = $response->data[0]->details->id;
292
293 // Save to WooCommerce meta.
294 update_post_meta( $product->get_id(), 'zcrm_product_id', $zoho_product_id );
295
296 return $zoho_product_id;
297 }
298
299 /**
300 * Update an existing product in Zoho CRM
301 *
302 * @param WC_Product $product WooCommerce product object
303 * @param string $zoho_product_id Zoho product ID
304 * @return bool True on success, false on failure
305 */
306 protected function update_zoho_product( $product, $zoho_product_id ) {
307 $product_payload = $this->format_product_for_zoho( $product, true );
308
309 $response = $this->zcrm_modules->cmbird_update_record( 'Products', $zoho_product_id, $product_payload );
310
311 if ( is_wp_error( $response ) || empty( $response->data[0]->details->id ) ) {
312 // Log error for debugging but remove for production.
313 return false;
314 }
315
316 return true;
317 }
318
319 /**
320 * Format WooCommerce product data for Zoho CRM
321 * Enhanced to handle variable products and variations
322 *
323 * @param WC_Product $product WooCommerce product object
324 * @param bool $update Whether this is an update operation
325 * @return array Formatted product data for Zoho API
326 */
327 protected function format_product_for_zoho( $product, $update = false ) {
328 // Get product categories as comma-separated string.
329 $category_names = array();
330 $category_ids = $product->get_category_ids();
331
332 if ( ! empty( $category_ids ) ) {
333 foreach ( $category_ids as $cat_id ) {
334 $term = get_term( $cat_id, 'product_cat' );
335 if ( $term && ! is_wp_error( $term ) ) {
336 $category_names[] = $term->name;
337 }
338 }
339 }
340
341 // Get product description.
342 $description = $product->get_description();
343 if ( empty( $description ) ) {
344 $description = $product->get_short_description();
345 }
346
347 // Get product SKU - handle variations differently.
348 $product_code = $product->get_sku();
349 if ( empty( $product_code ) ) {
350 $product_code = $product->get_id();
351 }
352
353 // Get stock quantity.
354 $stock_quantity = $product->get_stock_quantity();
355 if ( empty( $stock_quantity ) ) {
356 $stock_quantity = 0;
357 }
358
359 // Get product name - enhance for variations.
360 $product_name = $this->get_formatted_product_name( $product );
361
362 // Core product data.
363 $product_data = array(
364 'Product_Name' => $product_name,
365 'Product_Code' => $product_code,
366 'Unit_Price' => floatval( $product->get_price() ),
367 'Description' => $description,
368 'Product_Category' => ! empty( $category_names ) ? implode( ', ', $category_names ) : '',
369 'Product_Active' => 'publish' === $product->get_status(),
370 'Qty_in_Stock' => $stock_quantity,
371 'Handler' => get_option( 'cmbird_zoho_crm_owner_id', '' ),
372 );
373
374 // Add variation-specific data if this is a variation.
375 if ( $product->is_type( 'variation' ) ) {
376 $product_data = $this->add_variation_specific_data( $product, $product_data );
377 }
378
379 // Add variable product specific data if this is a variable product.
380 if ( $product->is_type( 'variable' ) ) {
381 $product_data = $this->add_variable_product_specific_data( $product, $product_data );
382 }
383
384 // Add custom mapped fields.
385 $custom_fields = $this->get_custom_mapped_fields_from_product( $product );
386 $product_data = array_merge( $product_data, $custom_fields );
387
388 // Remove empty values to avoid overwriting existing data in updates.
389 if ( $update ) {
390 $product_data = array_filter(
391 $product_data,
392 function ( $value ) {
393 return ! empty( $value ) || 0 === $value || false === $value;
394 }
395 );
396 }
397
398 return array( 'data' => array( $product_data ) );
399 }
400
401 /**
402 * Get formatted product name for Zoho (enhanced for variations)
403 *
404 * @param WC_Product $product WooCommerce product object
405 * @return string Formatted product name
406 */
407 protected function get_formatted_product_name( $product ) {
408 $name = $product->get_name();
409
410 // For variations, include attributes in the name.
411 if ( $product->is_type( 'variation' ) ) {
412 $parent_product = wc_get_product( $product->get_parent_id() );
413 if ( $parent_product ) {
414 $parent_name = $parent_product->get_name();
415 $attributes = $product->get_variation_attributes();
416
417 if ( ! empty( $attributes ) ) {
418 $attribute_parts = array();
419 foreach ( $attributes as $attribute_name => $attribute_value ) {
420 // Clean up attribute name (remove 'attribute_' prefix if present).
421 $clean_name = str_replace( 'attribute_', '', $attribute_name );
422 $clean_name = str_replace( 'pa_', '', $clean_name ); // Remove taxonomy prefix.
423 $clean_name = ucfirst( str_replace( array( '-', '_' ), ' ', $clean_name ) );
424
425 if ( ! empty( $attribute_value ) ) {
426 $attribute_parts[] = $clean_name . ': ' . $attribute_value;
427 }
428 }
429
430 if ( ! empty( $attribute_parts ) ) {
431 $name = $parent_name . ' - ' . implode( ', ', $attribute_parts );
432 }
433 }
434 }
435 }
436
437 return $name;
438 }
439
440 /**
441 * Add variation-specific data to product payload
442 *
443 * @param WC_Product_Variation $product Variation product object
444 * @param array $product_data Existing product data
445 * @return array Enhanced product data with variation info
446 */
447 protected function add_variation_specific_data( $product, $product_data ) {
448 // Get parent product ID for reference.
449 $parent_id = $product->get_parent_id();
450 if ( $parent_id ) {
451 $parent_zoho_id = get_post_meta( $parent_id, 'zcrm_product_id', true );
452 if ( $parent_zoho_id ) {
453 $product_data['Parent_Product_ID'] = $parent_zoho_id;
454 }
455 }
456
457 // Add variation attributes as additional fields.
458 $attributes = $product->get_variation_attributes();
459 if ( ! empty( $attributes ) ) {
460 $attribute_description = array();
461 foreach ( $attributes as $attribute_name => $attribute_value ) {
462 $clean_name = str_replace( array( 'attribute_', 'pa_' ), '', $attribute_name );
463 $clean_name = ucfirst( str_replace( array( '-', '_' ), ' ', $clean_name ) );
464 $attribute_description[] = $clean_name . ': ' . $attribute_value;
465 }
466
467 // Append to description.
468 if ( ! empty( $attribute_description ) ) {
469 $variation_info = 'Variation: ' . implode( ', ', $attribute_description );
470 $product_data['Description'] = ! empty( $product_data['Description'] )
471 ? $product_data['Description'] . "\n\n" . $variation_info
472 : $variation_info;
473 }
474 }
475
476 // Mark as variation.
477 $product_data['Product_Type'] = 'Variation';
478
479 return $product_data;
480 }
481
482 /**
483 * Add variable product specific data to product payload
484 *
485 * @param WC_Product_Variable $product Variable product object
486 * @param array $product_data Existing product data
487 * @return array Enhanced product data with variable product info
488 */
489 protected function add_variable_product_specific_data( $product, $product_data ) {
490 // Mark as variable product.
491 $product_data['Product_Type'] = 'Variable';
492
493 // Get variation count.
494 $variation_ids = $product->get_children();
495 $product_data['Variation_Count'] = count( $variation_ids );
496
497 // Get price range for variable products.
498 $min_price = $product->get_variation_price( 'min' );
499 $max_price = $product->get_variation_price( 'max' );
500
501 if ( $min_price !== $max_price ) {
502 $product_data['Price_Range'] = 'From ' . wc_price( $min_price ) . ' to ' . wc_price( $max_price );
503 $product_data['Min_Price'] = floatval( $min_price );
504 $product_data['Max_Price'] = floatval( $max_price );
505 }
506
507 // Add available attributes info to description.
508 $attributes = $product->get_variation_attributes();
509 if ( ! empty( $attributes ) ) {
510 $attribute_info = array();
511 foreach ( $attributes as $attribute_name => $attribute_values ) {
512 $clean_name = str_replace( array( 'attribute_', 'pa_' ), '', $attribute_name );
513 $clean_name = ucfirst( str_replace( array( '-', '_' ), ' ', $clean_name ) );
514 $attribute_info[] = $clean_name . ': ' . implode( ', ', $attribute_values );
515 }
516
517 if ( ! empty( $attribute_info ) ) {
518 $variation_info = 'Available Variations: ' . implode( '; ', $attribute_info );
519 $product_data['Description'] = ! empty( $product_data['Description'] )
520 ? $product_data['Description'] . "\n\n" . $variation_info
521 : $variation_info;
522 }
523 }
524
525 return $product_data;
526 }
527
528 /**
529 * Get custom mapped fields from product meta
530 *
531 * @param WC_Product $product WooCommerce product object
532 * @return array Custom field mappings
533 */
534 protected function get_custom_mapped_fields_from_product( $product ) {
535 $mapped_fields_json = get_option( 'zcrm_products_custom_fields' );
536 if ( empty( $mapped_fields_json ) ) {
537 return array();
538 }
539
540 $mapped_fields = json_decode( $mapped_fields_json, true );
541 if ( ! is_array( $mapped_fields ) ) {
542 return array();
543 }
544
545 $zoho_fields = array();
546
547 foreach ( $mapped_fields as $woo_meta_key => $zoho_field_api_name ) {
548 $value = get_post_meta( $product->get_id(), $woo_meta_key, true );
549
550 if ( ! empty( $value ) ) {
551 // Use Zoho CRM field api name as key in payload.
552 $zoho_fields[ $zoho_field_api_name ] = $value;
553 }
554 }
555
556 return $zoho_fields;
557 }
558
559 /**
560 * Bulk sync multiple products to Zoho CRM
561 * Enhanced to handle variable products and their variations
562 *
563 * @param array $product_ids Array of WooCommerce product IDs
564 * @return array Results with success/failure counts and details
565 */
566 public function cmbird_bulk_sync_products( $product_ids ) {
567 $results = array(
568 'success' => 0,
569 'failed' => 0,
570 'details' => array(),
571 'variable_products' => array(),
572 );
573
574 if ( empty( $product_ids ) || ! is_array( $product_ids ) ) {
575 return $results;
576 }
577
578 foreach ( $product_ids as $product_id ) {
579 $product = wc_get_product( $product_id );
580
581 if ( ! $product ) {
582 ++$results['failed'];
583 $results['details'][] = array(
584 'product_id' => $product_id,
585 'status' => 'error',
586 'message' => 'Product not found',
587 );
588 continue;
589 }
590
591 $sync_result = $this->cmbird_sync_product_to_zoho( $product );
592
593 // Handle different response types based on product type.
594 if ( $product->is_type( 'variable' ) && is_array( $sync_result ) ) {
595 // Variable product returns array with details.
596 $results['variable_products'][ $product_id ] = $sync_result;
597 $results['success'] += $sync_result['success_count'];
598 $results['failed'] += $sync_result['failed_count'];
599
600 $results['details'][] = array(
601 'product_id' => $product_id,
602 'product_type' => 'variable',
603 'parent_zoho_id' => $sync_result['parent_product_id'],
604 'variations_synced' => $sync_result['success_count'] - ( $sync_result['parent_product_id'] ? 1 : 0 ),
605 'variations_failed' => $sync_result['failed_count'] - ( $sync_result['parent_product_id'] ? 0 : 1 ),
606 'status' => $sync_result['success_count'] > 0 ? 'success' : 'error',
607 'message' => sprintf(
608 'Variable product sync: %d successful, %d failed',
609 $sync_result['success_count'],
610 $sync_result['failed_count']
611 ),
612 );
613 } elseif ( $sync_result ) {
614 // Simple product or variation returns single ID.
615 ++$results['success'];
616 $results['details'][] = array(
617 'product_id' => $product_id,
618 'product_type' => $product->get_type(),
619 'zoho_product_id' => $sync_result,
620 'status' => 'success',
621 'message' => 'Product synced successfully',
622 );
623 } else {
624 ++$results['failed'];
625 $results['details'][] = array(
626 'product_id' => $product_id,
627 'product_type' => $product->get_type(),
628 'status' => 'error',
629 'message' => 'Failed to sync product to Zoho CRM',
630 );
631 }
632
633 // Add a small delay to avoid rate limiting.
634 usleep( 200000 ); // 0.2 seconds.
635 }
636
637 return $results;
638 }
639
640 /**
641 * Get all products from Zoho CRM with pagination support
642 *
643 * @param int $page Page number (1-based)
644 * @param int $per_page Products per page (max 200)
645 * @return array|WP_Error Array of products or error
646 */
647 public function cmbird_get_all_zoho_products( $page = 1, $per_page = 200 ) {
648 $per_page = min( $per_page, 200 ); // Zoho limit.
649 $page = max( $page, 1 );
650
651 $url = $this->zoho_crm_url . 'crm/v7/Products?page=' . $page . '&per_page=' . $per_page;
652 $api = new CMBIRD_API_Handler_Zoho();
653 $response = $api->execute_curl_call_get( $url );
654
655 if ( is_wp_error( $response ) ) {
656 return $response;
657 }
658
659 return isset( $response->data ) ? $response->data : array();
660 }
661
662 /**
663 * Search products in Zoho CRM
664 *
665 * @param string $search_term Search term
666 * @param string $search_field Field to search in (optional)
667 * @return array|WP_Error Array of products or error
668 */
669 public function cmbird_search_zoho_products( $search_term, $search_field = null ) {
670 if ( empty( $search_term ) ) {
671 return new WP_Error( 'empty_search', 'Search term is required' );
672 }
673
674 if ( $search_field ) {
675 // Search in specific field.
676 $search_url = $this->zoho_crm_url . 'crm/v7/Products/search?criteria=' . rawurlencode( $search_field . ':contains:' . $search_term );
677 } else {
678 // General search.
679 $search_url = $this->zoho_crm_url . 'crm/v7/Products/search?word=' . rawurlencode( $search_term );
680 }
681
682 $api = new CMBIRD_API_Handler_Zoho();
683 $response = $api->execute_curl_call_get( $search_url );
684
685 if ( is_wp_error( $response ) ) {
686 return $response;
687 }
688
689 return isset( $response->data ) ? $response->data : array();
690 }
691
692 /**
693 * Get all available Zoho tax rates for a product
694 *
695 * @param WC_Product $product
696 * @return array
697 */
698 public function get_all_zoho_tax_rates_for_product( $product ) {
699 $tax_rates = array();
700
701 // Get all tax classes that apply to this product.
702 $tax_class = $product->get_tax_class();
703 $tax_rates_data = WC_Tax::get_rates_for_tax_class( $tax_class );
704
705 foreach ( $tax_rates_data as $rate_id => $rate_data ) {
706 $rate_code = WC_Tax::get_rate_code( $rate_id );
707 $mapped_tax = $this->get_zoho_tax_by_woo_tax_code( $rate_code );
708
709 if ( $mapped_tax && isset( $mapped_tax['id'] ) ) {
710 $tax_rates[] = array( 'id' => $mapped_tax['id'] ); // Array with id key and Zoho tax ID as value.
711 }
712 }
713
714 return $tax_rates;
715 }
716
717 /**
718 * Get Zoho product ID, creating if necessary (enhanced version from sales order class)
719 *
720 * @param WC_Product $product
721 * @param WC_Order $order Optional order for error notes
722 * @return string|null
723 */
724 public function get_zoho_product_id( $product, $order = null ) {
725 $woo_product_id = $product->get_id();
726 $zcrm_product_id = get_post_meta( $woo_product_id, 'zcrm_product_id', true );
727
728 if ( ! empty( $zcrm_product_id ) ) {
729 return $zcrm_product_id;
730 }
731
732 // Step 1: Try to find product in Zoho by name.
733 $search_url = $this->zoho_crm_url . 'crm/v7/Products/search?word=' . rawurlencode( $product->get_name() );
734 $api = new \CMBIRD_API_Handler_Zoho();
735 $response = $api->execute_curl_call_get( $search_url );
736
737 if ( ! is_wp_error( $response ) && ! empty( $response->data[0]->id ) ) {
738 $zcrm_product_id = $response->data[0]->id;
739 // Save to Woo meta.
740 update_post_meta( $woo_product_id, 'zcrm_product_id', $zcrm_product_id );
741 return $zcrm_product_id;
742 } else {
743 // Get all available tax rates for this product.
744 $tax_rates = $this->get_all_zoho_tax_rates_for_product( $product );
745
746 // Step 2: Create new product.
747 $product_payload = array(
748 'data' => array(
749 array(
750 'Product_Name' => $product->get_name(),
751 'Unit_Price' => round( floatval( $product->get_price() ), 2 ),
752 'Description' => wp_strip_all_tags( $product->get_description() ),
753 'Product_Code' => $product->get_sku() ?: $product->get_id(),
754 'Tax' => $tax_rates, // Include all available tax rates.
755 ),
756 ),
757 );
758
759 $this->log( 'CommerceBird Debug - Creating new product in Zoho: ' . $product->get_name() . ' (SKU: ' . ( $product->get_sku() ?: $product->get_id() ) . ')', 'debug' );
760 $this->log( 'CommerceBird Debug - Product Payload: ' . wp_json_encode( $product_payload ), 'debug' );
761
762 $create_response = $this->zcrm_modules->cmbird_create_record( 'Products', $product_payload );
763
764 // Log the full response for debugging.
765 $this->log( 'CommerceBird Debug - Zoho Product Creation Response: ' . wp_json_encode( $create_response ), 'debug' );
766
767 if ( is_wp_error( $create_response ) ) {
768 $error_message = 'Failed to create product in Zoho: ' . $product->get_name() . ' - ' . $create_response->get_error_message();
769 $this->log( 'CommerceBird Error - ' . $error_message, 'error' );
770
771 // Save error as order note if order is provided.
772 if ( $order ) {
773 $order->add_order_note( 'Zoho Product Creation Error: ' . $error_message );
774 }
775
776 return null;
777 }
778
779 // Check for empty response or missing ID.
780 if ( empty( $create_response->data[0]->details->id ) ) {
781 $error_message = 'Product creation returned empty ID for: ' . $product->get_name();
782
783 // Check if there are any error details in the response.
784 if ( isset( $create_response->data[0] ) ) {
785 $response_data = $create_response->data[0];
786
787 // Check for status error.
788 if ( isset( $response_data->status ) && $response_data->status === 'error' ) {
789 $zoho_error = isset( $response_data->message ) ? $response_data->message : 'Unknown Zoho error';
790 $error_message .= ' - Zoho Error: ' . $zoho_error;
791
792 // Check for detailed error information.
793 if ( isset( $response_data->details ) && is_object( $response_data->details ) ) {
794 if ( isset( $response_data->details->api_name ) ) {
795 $error_message .= ' (Field: ' . $response_data->details->api_name . ')';
796 }
797 }
798 }
799
800 // Check for code-based errors.
801 if ( isset( $response_data->code ) ) {
802 $error_message .= ' - Error Code: ' . $response_data->code;
803 }
804 }
805
806 $this->log( 'CommerceBird Error - ' . $error_message, 'error' );
807
808 // Save error as order note if order is provided.
809 if ( $order ) {
810 $order->add_order_note( 'Zoho Product Creation Error: ' . $error_message );
811 }
812
813 return null;
814 }
815
816 $zcrm_product_id = $create_response->data[0]->details->id;
817 }
818
819 // Save to Woo meta.
820 update_post_meta( $woo_product_id, 'zcrm_product_id', $zcrm_product_id );
821
822 return $zcrm_product_id;
823 }
824
825 /**
826 * Update Zoho product with all available tax rates
827 *
828 * @param WC_Product $product
829 * @param string $zcrm_product_id
830 * @param WC_Order $order Optional order for error notes
831 * @return void
832 */
833 public function update_zoho_product_with_tax_rates( $product, $zcrm_product_id, $order = null ) {
834 // Get all available tax rates for this product.
835 $tax_rates = $this->get_all_zoho_tax_rates_for_product( $product );
836
837 if ( empty( $tax_rates ) ) {
838 return; // No tax rates to update.
839 }
840
841 // Update the product in Zoho with tax rates.
842 $product_payload = array(
843 'data' => array(
844 array(
845 'id' => $zcrm_product_id,
846 'Product_Name' => $product->get_name(),
847 'Unit_Price' => round( floatval( $product->get_price() ), 2 ),
848 'Description' => wp_strip_all_tags( $product->get_description() ),
849 'Product_Code' => $product->get_sku() ?: $product->get_id(),
850 'Product_Category' => $product->get_category_ids() ? implode( ',', $product->get_category_ids() ) : '',
851 'Tax' => $tax_rates, // Include all available tax rates.
852 ),
853 ),
854 );
855
856 $update_response = $this->zcrm_modules->cmbird_update_record( 'Products', $zcrm_product_id, $product_payload );
857
858 // Log any errors but don't fail the order sync.
859 if ( is_wp_error( $update_response ) ) {
860 $error_message = 'Failed to update Zoho product with tax rates: ' . $update_response->get_error_message();
861 $this->log( $error_message, 'error' );
862
863 // Save error as order note if order is provided.
864 if ( $order ) {
865 $order->add_order_note( 'Zoho Product Update Error: ' . $error_message );
866 }
867 }
868 }
869
870 /**
871 * Get Zoho tax mapping by WooCommerce tax code
872 *
873 * @param string $woo_tax_code
874 * @return array|null
875 */
876 public function get_zoho_tax_by_woo_tax_code( $woo_tax_code ) {
877 if ( ! $woo_tax_code ) {
878 return null;
879 }
880 // get the rate code from the woo_tax_id last integer value after the last dash.
881 $woo_tax_id = intval( preg_replace( '/.*-(\d+)$/', '$1', $woo_tax_code ) );
882 if ( ! $woo_tax_id ) {
883 return null;
884 }
885
886 $option = get_option( 'cmbird_zoho_crm_tax_rate_' . $woo_tax_id );
887 if ( ! $option ) {
888 return null;
889 }
890
891 $parts = explode( '|', $option );
892 if ( count( $parts ) < 1 ) {
893 return null;
894 }
895 // format the name by replacing the @@ with a space.
896 $name = isset( $parts[1] ) ? str_replace( '@@', ' ', $parts[1] ) : '';
897
898 return array(
899 'id' => $parts[0],
900 'name' => $name,
901 'value' => isset( $parts[2] ) ? $parts[2] : '',
902 );
903 }
904 }
905
906 // Initialize the class.
907 $cmbird_zcrm_product = new CMBIRD_ZCRM_Product();
908