PluginProbe ʕ •ᴥ•ʔ
CommerceBird – AI Command Center, ERP Integrations & B2B for WooCommerce (Zoho, Exact Online). / 2.8.0
CommerceBird – AI Command Center, ERP Integrations & B2B for WooCommerce (Zoho, Exact Online). v2.8.0
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 / admin / includes / Actions / Ajax / ZohoInventoryAjax.php
commercebird / admin / includes / Actions / Ajax Last commit date
AcfAjax.php 9 months ago ExactOnlineAjax.php 5 months ago SettingsAjax.php 4 months ago ZohoCRMAjax.php 5 months ago ZohoInventoryAjax.php 5 months ago index.php 1 year ago
ZohoInventoryAjax.php
1316 lines
1 <?php
2
3 namespace CommerceBird\Admin\Actions\Ajax;
4
5 use CMBIRD_Auth_Zoho;
6 use CMBIRD_API_Handler_Zoho;
7 use CMBIRD_Pricelist_ZI;
8 use CMBIRD_Categories_ZI;
9 use CommerceBird\Admin\Template;
10 use CommerceBird\Admin\Traits\AjaxRequest;
11 use CommerceBird\Admin\Traits\OptionStatus;
12 use CommerceBird\Admin\Traits\Singleton;
13 use CommerceBird\Admin\Traits\LogWriter;
14 use Throwable;
15 use WC_Tax;
16 use function gettype;
17
18 if ( ! defined( 'ABSPATH' ) ) {
19 exit;
20 }
21 final class ZohoInventoryAjax {
22
23
24 use Singleton;
25 use AjaxRequest;
26 use OptionStatus;
27 use LogWriter;
28
29 private const FORMS = array(
30 'tax' => array(
31 'selectedTaxRates',
32 ),
33 'product' => array(
34 'item_from_zoho',
35 'disable_stock_sync',
36 'disable_product_sync',
37 'enable_accounting_stock',
38 ),
39 'cron' => array(
40 'form',
41 'categories',
42 ),
43 'order' => array(
44 'disable_sync',
45 'enable_auto_number',
46 'enable_order_status',
47 'enable_sales_order_cycle',
48 'order_prefix',
49 'location_id',
50 'enable_locationstock',
51 'location_country_map',
52 ),
53 'contact' => array(
54 'enable_cron',
55 ),
56 'price' => array(
57 'wp_user_role',
58 'zoho_inventory_pricelist',
59 'wcb2b',
60 ),
61 );
62
63 private const ACTIONS = array(
64 'get_zoho_connect' => 'connection_get',
65 'save_zoho_connect' => 'connection_set',
66 'reset_zoho_connect' => 'connection_reset',
67 'get_zoho_tax' => 'tax_get',
68 'save_zoho_tax' => 'tax_set',
69 'reset_zoho_tax' => 'tax_reset',
70 'get_zoho_product' => 'product_get',
71 'save_zoho_product' => 'product_set',
72 'reset_zoho_product' => 'product_reset',
73 'get_zoho_cron' => 'cron_get',
74 'save_zoho_cron' => 'cron_set',
75 'reset_zoho_cron' => 'cron_reset',
76 'get_zoho_order' => 'order_get',
77 'save_zoho_order' => 'order_set',
78 'reset_zoho_order' => 'order_reset',
79 'get_zoho_contact' => 'contact_get',
80 'save_zoho_contact' => 'contact_set',
81 'reset_zoho_contact' => 'contact_reset',
82 'get_zoho_price' => 'price_get',
83 'save_zoho_price' => 'price_set',
84 'reset_zoho_price' => 'price_reset',
85 'get_zoho_fields' => 'fields_get',
86 'save_zoho_fields' => 'fields_set',
87 'reset_zoho_fields' => 'fields_reset',
88 'is_connected' => 'connection_done',
89 'get_wc_taxes' => 'wc_tax_collect',
90 'get_zoho_taxes' => 'zoho_tax_rates_collect',
91 'get_zoho_categories' => 'zoho_categories_collect',
92 'get_zoho_locations' => 'zoho_locations_collect',
93 'get_zoho_prices' => 'zoho_prices_collect',
94 'get_all_custom_fields' => 'wc_custom_fields_collect',
95 'get_zoho_webhooks' => 'get_zoho_webhooks',
96 'handle_code' => 'handle_code',
97 'convert_guest' => 'convert_guest',
98 'zoho_ajax_call_variable_item_from_zoho' => 'sync_variable_items_from_zoho',
99 'zoho_ajax_call_item_from_zoho' => 'sync_simple_items_from_zoho',
100 'zoho_ajax_call_item' => 'sync_items_to_zoho',
101 'import_zoho_contacts' => 'sync_contacts_from_zoho',
102 'zoho_ajax_call_composite_item_from_zoho' => 'sync_composite_items_from_zoho',
103 'zoho_ajax_call_composite_item' => 'sync_composite_items_to_zoho',
104 );
105
106 private const SOURCE = 'zoho';
107
108 /**
109 * ZohoInventoryAjax constructor.
110 */
111 public function __construct() {
112 $this->load_actions();
113 }
114
115 /**
116 * Collects custom checkout fields for WooCommerce.
117 *
118 * This function verifies the request and collects custom fields from the
119 * THWCFD_Utils::get_checkout_fields() function if the THWCFD class exists.
120 *
121 * @return void
122 */
123 public function wc_custom_fields_collect(): void {
124 $this->verify();
125 $types = array( 'billing', 'shipping', 'additional' );
126 $all_fields = array();
127 $custom_fields = array();
128 // Get all the fields.
129 foreach ( $types as $type ) {
130 // Skip if an unsupported type.
131 if (
132 ! in_array(
133 $type,
134 array(
135 'billing',
136 'shipping',
137 'additional',
138 ),
139 true,
140 )
141 ) {
142 continue;
143 }
144
145 $temp_fields = get_option( 'wc_fields_' . $type );
146 if ( false !== $temp_fields ) {
147 $all_fields = array_merge( $all_fields, $temp_fields );
148 }
149 }
150 // Loop through each field to see if it is a custom field.
151 foreach ( $all_fields as $name => $options ) {
152 if ( isset( $options['custom'] ) && $options['custom'] ) {
153 $label = trim( $options['label'] );
154 $custom_fields[ $name ] = empty( $label ) ? __( 'Please set label', 'commercebird' ) : $label;
155 }
156 }
157
158 $this->response = $custom_fields;
159
160 $this->serve();
161 }
162
163 /**
164 * Retrieves the connection details.
165 *
166 * @return void
167 */
168 public function connection_get(): void {
169 $this->verify();
170 $this->response['account_domain'] = get_option( 'cmbird_zoho_inventory_domain', '' );
171 $this->response['organization_id'] = get_option( 'cmbird_zoho_inventory_oid', '' );
172 $this->response['client_id'] = get_option( 'cmbird_zoho_inventory_cid', '' );
173 $this->response['client_secret'] = get_option( 'cmbird_zoho_inventory_cs', '' );
174 $this->response['inventory_url'] = get_option( 'cmbird_zoho_inventory_url', '' );
175 $this->response['redirect_uri'] = get_option( 'cmbird_authorization_redirect_uri', '' );
176 $this->serve();
177 }
178
179 /**
180 * Retrieves the fields and serves the response.
181 *
182 * @return void
183 */
184 public function fields_get(): void {
185 $this->verify();
186 $this->response['form'] = get_option( 'cmbird_wootozoho_custom_fields', array() );
187 $this->serve();
188 }
189
190 /**
191 * Sets the custom fields for the form.
192 *
193 * @return void
194 */
195 public function fields_set(): void {
196 $this->verify( array( 'form' ) );
197 try {
198 update_option( 'cmbird_wootozoho_custom_fields', $this->data['form'] );
199 $this->response = array( 'message' => 'saved' );
200 } catch ( Throwable $throwable ) {
201 $this->errors = array( 'message' => $throwable->getMessage() );
202 }
203 $this->serve();
204 }
205
206 /**
207 * Resets the fields.
208 *
209 * @return void
210 */
211 public function fields_reset(): void {
212 $this->verify();
213 delete_option( 'cmbird_wootozoho_custom_fields' );
214 $this->response = array( 'message' => 'Reset successfully!' );
215 $this->serve();
216 }
217
218 /**
219 * Resets the price.
220 *
221 * @return void
222 */
223 public function price_reset(): void {
224 $this->verify();
225 delete_option( 'cmbird_zoho_pricelist_id' );
226 delete_option( 'cmbird_zoho_pricelist_role' );
227 delete_option( 'cmbird_zoho_pricelist_wcb2b_groups' );
228 $this->response = array( 'message' => 'Reset successfully!' );
229 $this->serve();
230 }
231
232 /**
233 * Retrieves the price information.
234 *
235 * @return void
236 */
237 public function price_get(): void {
238 $this->verify();
239 $this->response['zoho_inventory_pricelist'] = get_option( 'cmbird_zoho_pricelist_id' );
240 $this->response['wp_user_role'] = get_option( 'cmbird_zoho_pricelist_role' );
241 if ( class_exists( 'WooCommerceB2B' ) ) {
242 $this->response['wcb2b'] = get_option( 'cmbird_zoho_pricelist_wcb2b_groups' );
243 }
244 $this->serve();
245 }
246
247 /**
248 * Sets the price and saves the pricelist.
249 */
250 public function price_set(): void {
251 $this->verify( self::FORMS['price'] );
252 try {
253 $import_pricelist = new CMBIRD_Pricelist_ZI();
254 $success = $import_pricelist->save_price_list( $this->data );
255 if ( class_exists( 'Addify_B2B_Plugin' ) ) {
256 update_option( 'cmbird_zoho_pricelist_role', $this->data['wp_user_role'] );
257 } else {
258 update_option( 'cmbird_zoho_pricelist_wcb2b_groups', $this->data['wcb2b'] );
259 }
260 if ( $success ) {
261 $this->response = array( 'message' => 'Saved' );
262 if ( class_exists( 'WooCommerceB2B' ) ) {
263 $this->response['wcb2b'] = $this->data['wcb2b'];
264 }
265 } else {
266 $this->errors = array( 'message' => 'Failed to save' );
267 }
268 } catch ( Throwable $throwable ) {
269 $this->errors = array( 'message' => $throwable->getMessage() );
270 $this->write_log(
271 array(
272 'message' => $throwable->getMessage(),
273 'files' => wp_list_pluck( $throwable->getTrace(), 'file', 'line' ),
274 ),
275 'zoho-ajax-error'
276 );
277 }
278
279 $this->serve();
280 }
281
282 /**
283 * Collects Zoho prices.
284 *
285 * @return void
286 */
287 public function zoho_prices_collect(): void {
288 $this->verify();
289 $price_list_class = new CMBIRD_Pricelist_ZI();
290 $prices = $price_list_class->zi_get_all_pricelist();
291 $this->response = wp_list_pluck( $prices, 'name', 'pricebook_id' );
292 $this->serve();
293 }
294
295 /**
296 * Retrieves the contact details.
297 *
298 * @return void
299 */
300 public function contact_get() {
301 $this->verify();
302 $this->response = $this->option_status_get( self::FORMS['contact'], self::SOURCE );
303 $this->serve();
304 }
305 /**
306 * Sets the contact and updates the status.
307 *
308 * @return void
309 */
310 public function contact_set() {
311 $this->verify( self::FORMS['contact'] );
312 $this->option_status_update( $this->data, self::SOURCE );
313 if ( isset( $this->data['enable'] ) && ! empty( $this->data['enable'] ) ) {
314 if ( ! wp_next_scheduled( 'zoho_contact_sync' ) ) {
315 wp_schedule_event( time(), 'daily', 'zoho_contact_sync' );
316 }
317 } else {
318 wp_clear_scheduled_hook( 'zoho_contact_sync' );
319 }
320 $this->response = array( 'message' => 'Saved!' );
321 $this->serve();
322 }
323 /**
324 * Resets the contact.
325 *
326 * @return void
327 */
328 public function contact_reset() {
329 $this->verify();
330 $this->option_status_remove( self::FORMS['contact'], self::SOURCE );
331 $this->response = array( 'message' => 'Reset successfully!' );
332 $this->serve();
333 }
334
335 /**
336 * Sets the order and updates the status.
337 *
338 * @return void
339 */
340 public function order_set(): void {
341 $this->verify( self::FORMS['order'] );
342 $this->option_status_update( $this->data, self::SOURCE );
343 $this->response = array( 'message' => 'Saved!' );
344 $this->serve();
345 }
346
347 /**
348 * Retrieves the order.
349 *
350 * @return void
351 */
352 public function order_get(): void {
353 $this->verify();
354 $this->response = $this->option_status_get( self::FORMS['order'], self::SOURCE );
355 // option_status_get defaults missing options to ''. Ensure location_country_map.
356 // is always returned as an object so the frontend can rely on a consistent type.
357 if ( ! is_array( $this->response['location_country_map'] ) ) {
358 $this->response['location_country_map'] = new \stdClass();
359 }
360 $this->serve();
361 }
362
363 /**
364 * Resets the order.
365 *
366 * @return void
367 */
368 public function order_reset(): void {
369 $this->verify();
370 $this->option_status_remove( self::FORMS['order'], self::SOURCE );
371 $this->response = array( 'message' => 'Reset successfully!' );
372 $this->serve();
373 }
374
375 /**
376 * Collects Zoho locations.
377 *
378 * This function verifies the request, retrieves the Zoho inventory
379 * organization ID and URL from the options, constructs the URL
380 * for the API call, and executes the cURL call to retrieve the
381 * locations. The response is then processed to extract the
382 * location names and IDs, and stored in the class property.
383 *
384 * @return void
385 */
386 public function zoho_locations_collect(): void {
387 // $fd = fopen( __DIR__ . '/zoho_locations.txt', 'w+' );
388 $this->verify();
389 $zoho_inventory_oid = get_option( 'cmbird_zoho_inventory_oid' );
390 $zoho_inventory_url = get_option( 'cmbird_zoho_inventory_url' );
391 $url = $zoho_inventory_url . 'inventory/v1/locations?organization_id=' . $zoho_inventory_oid;
392 $execute_curl_call_handle = new CMBIRD_API_Handler_Zoho();
393 $json = $execute_curl_call_handle->execute_curl_call_get( $url );
394 // save the first location id to the database of the first object of json.
395 if ( ! empty( $json->locations ) && is_array( $json->locations ) && array_key_exists( 0, $json->locations ) ) {
396 $location_id = $json->locations[0]->location_id;
397 update_option( 'cmbird_zoho_parent_location_id_status', $location_id );
398 }
399 $this->response = wp_list_pluck( $json->locations, 'location_name', 'location_id' );
400 $this->serve();
401 }
402
403 /**
404 * Sets the cron for the class.
405 */
406 public function cron_set(): void {
407 $this->verify( self::FORMS['cron'] );
408 if ( array_key_exists( 'form', $this->data ) ) {
409 $decode = json_decode( $this->data['form'], true );
410 update_option( 'zi_cron_interval', $decode['zi_cron_interval'] );
411 unset( $decode['zi_cron_interval'] );
412 $this->option_status_update( $decode, self::SOURCE );
413 }
414 if ( array_key_exists( 'categories', $this->data ) ) {
415 $decode = json_decode( $this->data['categories'] );
416 update_option( 'cmbird_zoho_item_category', serialize( $decode ) );
417 }
418 $this->response = array( 'message' => 'Saved' );
419 $this->serve();
420 }
421
422 /**
423 * Executes the cron_get function.
424 *
425 * This function verifies the form status and retrieves the form options
426 * for syncing name, price, image, and description. It also retrieves
427 * the cron interval and the Zoho item categories.
428 *
429 * @return void
430 */
431 public function cron_get(): void {
432 $this->verify();
433 $this->response = array();
434 $this->response['form'] = $this->option_status_get(
435 array(
436 'disable_name_sync',
437 'disable_price_sync',
438 'disable_image_sync',
439 'disable_description_sync',
440 ),
441 self::SOURCE,
442 );
443 $this->response['form']['zi_cron_interval'] = get_option( 'zi_cron_interval', 'none' );
444 $categories = get_option( 'cmbird_zoho_item_category', '' );
445 $this->response['categories'] = maybe_unserialize( $categories );
446 $this->serve();
447 }
448
449 /**
450 * Resets the cron job.
451 *
452 * This function verifies the cron job, removes the specified options,
453 * deletes the 'zoho_item_category' option, and serves the cron job.
454 *
455 * @return void
456 */
457 public function cron_reset(): void {
458 $this->verify();
459 $this->option_status_remove(
460 array(
461 'disable_name_sync',
462 'disable_price_sync',
463 'disable_image_sync',
464 'disable_description_sync',
465 ),
466 self::SOURCE,
467 );
468 delete_option( 'cmbird_zoho_item_category' );
469 // delete all options that contain 'zoho_id_for_term_id_'.
470 global $wpdb;
471
472 // First, count how many matching options exist.
473 $count_before = $wpdb->get_var(
474 $wpdb->prepare(
475 "SELECT COUNT(*) FROM {$wpdb->prefix}options WHERE option_name LIKE %s",
476 'cmbird_zoho_id_for_term_id_%'
477 )
478 );
479
480 // Now delete them.
481 $deleted_rows = $wpdb->query(
482 $wpdb->prepare(
483 "DELETE FROM {$wpdb->prefix}options WHERE option_name LIKE %s",
484 'cmbird_zoho_id_for_term_id_%'
485 )
486 );
487
488 $this->response = array( 'message' => "Reset successfully! Found {$count_before} category mapping options, deleted {$deleted_rows}." );
489 $this->serve();
490 }
491
492 /**
493 * Collects all categories from both Zoho Inventory and WooCommerce.
494 *
495 * This function retrieves categories from Zoho Inventory and merges them with
496 * WooCommerce categories to provide a complete list for the frontend display.
497 * This ensures all 66+ categories are visible, not just the ones in Zoho.
498 */
499 public function zoho_categories_collect(): void {
500 $this->verify();
501 $category_class = new CMBIRD_Categories_ZI();
502
503 // Only return Zoho categories since we're storing Zoho category IDs in options.
504 $zoho_categories = $category_class->cmbird_get_zoho_item_categories();
505 if ( gettype( $zoho_categories ) === 'array' ) {
506 $this->response = $zoho_categories;
507 } else {
508 $this->response = array();
509 }
510 $this->serve();
511 }
512
513 /**
514 * Executes the connection process and handles the response.
515 */
516 public function connection_done(): void {
517 $this->verify();
518 $zoho_inventory_url = get_option( 'cmbird_zoho_inventory_url' );
519 $zoho_inventory_oid = get_option( 'cmbird_zoho_inventory_oid' );
520 $url = $zoho_inventory_url . 'inventory/v1/apiusagereport/dashboard?version=all&organization_id=' . $zoho_inventory_oid;
521
522 $execute_curl_call_handle = new CMBIRD_API_Handler_Zoho();
523 $json = $execute_curl_call_handle->execute_curl_call_get( $url );
524
525 if ( is_wp_error( $json ) ) {
526 $this->errors = array( 'message' => $json->get_error_message() );
527 return;
528 }
529
530 if ( empty( $json ) || empty( $json->data ) || empty( $json->data->api_usage ) ) {
531 $this->errors = array( 'message' => 'We lost connection with Zoho. Please refresh the page.' );
532 return;
533 }
534 $api_usage = $json->data->api_usage;
535 // Debug JSON encoding.
536 $encoded_api_usage = wp_json_encode( $api_usage );
537 // Ensure it's not null.
538 $this->response = json_decode( $encoded_api_usage, true ) ?? array();
539 $this->serve();
540 }
541
542 /**
543 * Executes the product_set function.
544 */
545 public function product_set(): void {
546 $this->verify( self::FORMS['product'] );
547 if ( ! empty( $this->data ) ) {
548 $this->option_status_update( $this->data, self::SOURCE );
549 }
550 $this->response = array( 'message' => 'Saved!' );
551 $this->serve();
552 }
553
554 /**
555 * Retrieves the product data.
556 *
557 * @return void
558 */
559 public function product_get(): void {
560 $this->verify();
561 $this->response = $this->option_status_get( self::FORMS['product'], self::SOURCE );
562 $this->serve();
563 }
564
565 /**
566 * Resets the product.
567 *
568 * @return void
569 */
570 public function product_reset(): void {
571 $this->verify();
572 $this->option_status_remove( self::FORMS['product'], self::SOURCE );
573 $this->response = array( 'message' => 'Reset successfully!' );
574 $this->serve();
575 }
576
577 /**
578 * Retrieves the tax information.
579 *
580 * @return void
581 */
582 public function tax_get(): void {
583 $this->verify();
584 $this->response = array();
585 foreach ( $this->wc_taxes() as $tax ) {
586 $this->response['selectedTaxRates'][] = $tax['id'] . '^^' . get_option( 'cmbird_zoho_inventory_tax_rate_' . $tax['id'] );
587 }
588 $this->serve();
589 }
590
591 /**
592 * Retrieves an array of all tax rates from WooCommerce.
593 *
594 * @return array An array of all tax rates.
595 */
596 public function wc_taxes(): array {
597 $wc_tax_array = array();
598 $tax_classes = WC_Tax::get_tax_classes(); // Retrieve all tax classes.
599 if ( ! in_array( '', $tax_classes ) ) { // Make sure "Standard rate" (empty class name) is present.
600 array_unshift( $tax_classes, '' );
601 }
602
603 foreach ( $tax_classes as $tax_class ) { // For each tax class, get all rates.
604 $taxes = WC_Tax::get_rates_for_tax_class( $tax_class );
605
606 foreach ( $taxes as $key => $tax ) {
607 $taxarray = (array) $tax;
608 $taxarray['id'] = $key;
609 $wc_tax_array[] = $taxarray;
610 }
611 }
612
613 return $wc_tax_array;
614 }
615
616 /**
617 * Resets the tax settings.
618 *
619 * This function verifies the tax settings and then deletes the options
620 * related to the tax rates and tax groups.
621 *
622 * @return void
623 */
624 public function tax_reset(): void {
625 $this->verify();
626 foreach ( $this->wc_taxes() as $tax ) {
627 delete_option( 'cmbird_zoho_inventory_tax_rate_' . $tax['id'] );
628 }
629 delete_option( 'cmbird_zi_tax_groups' );
630 $this->response = array( 'message' => 'Reset successfully!' );
631 $this->serve();
632 }
633
634 /**
635 * Saves tax rates to the database.
636 */
637 public function tax_set(): void {
638 $this->verify( self::FORMS['tax'] );
639 $rates = array_filter( $this->data['selectedTaxRates'] );
640 try {
641 foreach ( $rates as $value ) {
642 $valarray = explode( '^^', $value );
643 update_option( 'cmbird_zoho_inventory_tax_rate_' . $valarray[0], $valarray[1] );
644 }
645 $this->response = array( 'message' => 'Saved' );
646 } catch ( Throwable $throwable ) {
647 $this->errors = array( 'message' => $throwable->getMessage() );
648 }
649
650 $this->serve();
651 }
652
653 /**
654 * Sets the connection for the Zoho Inventory API.
655 */
656 public function connection_set(): void {
657 $this->verify(
658 array(
659 'account_domain',
660 'organization_id',
661 'client_id',
662 'client_secret',
663 'redirect_uri',
664 ),
665 );
666 try {
667 $domain = CMBIRD_Auth_Zoho::normalize_domain( $this->data['account_domain'] );
668 $inventory = sprintf( 'https://www.zohoapis.%s/', $domain );
669 $oauth_args = array(
670 'response_type' => 'code',
671 'client_id' => $this->data['client_id'],
672 'scope' => 'ZohoInventory.FullAccess.all',
673 'redirect_uri' => $this->data['redirect_uri'],
674 'prompt' => 'consent',
675 'access_type' => 'offline',
676 'state' => wp_create_nonce( Template::NAME ),
677 );
678
679 update_option( 'cmbird_zoho_inventory_domain', $domain );
680 update_option( 'cmbird_zoho_inventory_oid', $this->data['organization_id'] );
681 update_option( 'cmbird_zoho_inventory_cid', $this->data['client_id'] );
682 update_option( 'cmbird_zoho_inventory_cs', $this->data['client_secret'] );
683 update_option( 'cmbird_zoho_inventory_url', $inventory );
684 update_option( 'cmbird_authorization_redirect_uri', $this->data['redirect_uri'] );
685 $redirect_base = CMBIRD_Auth_Zoho::get_auth_redirect_url_for_domain( $domain );
686 $redirect = esc_url_raw( add_query_arg( $oauth_args, $redirect_base ) );
687 $this->response = array(
688 'redirect' => $redirect,
689 'message' => 'We are redirecting you to zoho. please wait...',
690 );
691 } catch ( Throwable $throwable ) {
692 $this->errors = array( 'message' => $throwable->getMessage() );
693 }
694
695 $this->serve();
696 }
697
698 /**
699 * Resets the connection by deleting specific options from the database.
700 */
701 public function connection_reset(): void {
702 $this->verify();
703 try {
704 // Check if resetAll is requested (sent from Vue).
705 $reset_all = isset( $this->data['reset'] ) && ( $this->data['reset'] === 1 || $this->data['reset'] === true || $this->data['reset'] === 'true' );
706
707 // Safety: require an explicit confirmation token for destructive, global reset.
708 $confirm_token = isset( $this->data['confirm'] ) ? $this->data['confirm'] : '';
709
710 if ( $reset_all ) {
711 // The client must pass confirm: 'reset-all' to perform the full destructive reset.
712 if ( 'reset-all' !== $confirm_token ) {
713 $this->errors = array( 'message' => 'Missing reset confirmation.' );
714 $this->serve();
715 return;
716 }
717
718 // Audit log for full reset (important to have server-side evidence).
719 $this->write_log(
720 array(
721 'action' => 'full_reset_triggered',
722 'user_id' => get_current_user_id(),
723 ),
724 'zoho-reset'
725 );
726
727 global $wpdb;
728 // Delete all product and customer meta keys.
729 $wpdb->query( "DELETE FROM {$wpdb->prefix}postmeta WHERE meta_key IN ('zi_item_id', 'zi_category_id')" );
730 $wpdb->query( "DELETE FROM {$wpdb->prefix}usermeta WHERE meta_key IN ('zi_contact_id', 'zi_primary_contact_id', 'zi_billing_address_id', 'zi_shipping_address_id', 'zi_contact_persons_id')" );
731
732 // Delete all plugin options that belong to Zoho namespace.
733 $wpdb->query(
734 $wpdb->prepare(
735 "DELETE FROM {$wpdb->prefix}options WHERE option_name LIKE %s",
736 'cmbird_zoho_%'
737 )
738 );
739
740 $this->response['message'] = __( 'All settings are reset successfully', 'commercebird' );
741 $this->serve();
742 return;
743 }
744
745 // Non-destructive reset (connection settings only).
746 $options = array(
747 'cmbird_zoho_inventory_domain',
748 'cmbird_zoho_inventory_oid',
749 'cmbird_zoho_inventory_cid',
750 'cmbird_zoho_inventory_cs',
751 'cmbird_zoho_inventory_url',
752 'cmbird_zoho_inventory_access_token',
753 );
754 foreach ( $options as $zi_option ) {
755 delete_option( $zi_option );
756 }
757 $this->response = array( 'message' => 'Reset successfully!' );
758
759 } catch ( Throwable $throwable ) {
760 $this->errors = array( 'message' => $throwable->getMessage() );
761 }
762
763 $this->serve();
764 }
765
766 /**
767 * Retrieves the Zoho tax rates for the organization.
768 *
769 * This function verifies the request, retrieves the organization ID and URL
770 * from the options, constructs the URL for the API request, and executes
771 * the cURL call to fetch the tax rates. If the response contains the
772 * 'taxes' key, it sets the response to the tax rates and saves tax groups
773 * separately. Otherwise, it sets the errors to a default message.
774 */
775 public function zoho_tax_rates_collect(): void {
776 $this->verify();
777 $zoho_inventory_oid = get_option( 'cmbird_zoho_inventory_oid' );
778 $zoho_inventory_url = get_option( 'cmbird_zoho_inventory_url' );
779 $url = $zoho_inventory_url . 'inventory/v1/settings/taxes?organization_id=' . $zoho_inventory_oid;
780 try {
781 $execute_curl_call_handle = new CMBIRD_API_Handler_Zoho();
782 $json = (array) $execute_curl_call_handle->execute_curl_call_get( $url );
783 if ( array_key_exists( 'taxes', $json ) ) {
784 $this->response = $json['taxes'];
785
786 // Save tax groups separately for compound tax handling.
787 $tax_groups = array();
788 foreach ( $json['taxes'] as $tax ) {
789 if ( isset( $tax->tax_type ) && 'tax_group' === $tax->tax_type ) {
790 $tax_groups[] = array(
791 'tax_id' => $tax->tax_id,
792 'tax_percentage' => $tax->tax_percentage,
793 'tax_name' => $tax->tax_name,
794 );
795 }
796 }
797 update_option( 'cmbird_zi_tax_groups', $tax_groups, false );
798 } else {
799 $this->errors = array( 'message' => 'Something went wrong!' );
800 }
801 } catch ( Throwable $throwable ) {
802 $this->errors = array( 'message' => $throwable->getMessage() );
803 }
804
805 $this->serve();
806 }
807
808 /**
809 * Executes the wc_tax_collect function.
810 *
811 * This function verifies the current state, retrieves the taxes using the
812 * wc_taxes method, and serves the response.
813 */
814 public function wc_tax_collect(): void {
815 $this->verify();
816 $this->response = $this->wc_taxes();
817 $this->serve();
818 }
819
820 /**
821 * Handles the oAuth code.
822 */
823 public function handle_code(): void {
824 $this->verify();
825 if ( array_key_exists( 'code', $this->request ) ) {
826 $class_functions = new CMBIRD_Auth_Zoho();
827 $code = $this->request['code'];
828 $state = $this->request['state'] ?? '';
829
830 if ( empty( $state ) || ! wp_verify_nonce( $state, Template::NAME ) ) {
831 $this->errors = array( 'message' => 'Invalid OAuth state. Please try connecting again.' );
832 $this->serve();
833 return;
834 }
835
836 try {
837 $access_token = $class_functions->get_zoho_access_token( $code, 'zoho_inventory' );
838 if ( is_wp_error( $access_token ) ) {
839 $this->errors = array( 'message' => $access_token->get_error_message() );
840 } elseif ( is_array( $access_token ) && array_key_exists( 'access_token', $access_token ) ) {
841 update_option( 'cmbird_zoho_inventory_auth_code', $code );
842 update_option( 'cmbird_zoho_inventory_access_token', $access_token['access_token'] );
843 update_option( 'cmbird_zoho_inventory_refresh_token', $access_token['refresh_token'] );
844 update_option( 'cmbird_zoho_inventory_timestamp', strtotime( gmdate( 'Y-m-d H:i:s' ) ) + $access_token['expires_in'] );
845 $this->response = (array) $access_token;
846 } else {
847 $this->errors = array( 'message' => 'Unable to retrieve Zoho Inventory access token.' );
848 }
849 } catch ( Throwable $throwable ) {
850 $this->errors = array( 'message' => $throwable->getMessage() );
851 }
852 }
853 $this->serve();
854 }
855
856 /**
857 * Converts all guest users to a registered users.
858 *
859 * This function verifies the request, retrieves all guest orders
860 *
861 * @return void
862 */
863 public function convert_guest(): void {
864 $this->verify();
865 $args = array(
866 'status' => array( 'wc-processing', 'wc-completed' ),
867 'return' => 'ids',
868 'customer' => 0, // Only guest orders.
869 'limit' => -1, // Retrieve all guest orders.
870 );
871
872 $orders = wc_get_orders( $args );
873 if ( empty( $orders ) ) {
874 $this->response = array( 'message' => __( 'No guest orders found.', 'commercebird' ) );
875 $this->serve();
876 return;
877 }
878 $count = count( $orders );
879 foreach ( $orders as $order_id ) {
880 $order = wc_get_order( $order_id );
881 $email = $order->get_billing_email();
882
883 if ( email_exists( $email ) === false ) {
884 $username = strstr( $email, '@', true );
885 $password = wp_generate_password();
886 $user_id = wc_create_new_customer( $email, $username, $password );
887
888 if ( ! is_wp_error( $user_id ) ) {
889 // use update_meta to set the user ID for the order.
890 $order->set_customer_id( $user_id );
891 $order->save();
892 }
893 } else {
894 // If the email already exists, we can still update the order to the existing user.
895 $user = get_user_by( 'email', $email );
896 if ( $user ) {
897 $order->set_customer_id( $user->ID );
898 $order->save();
899 }
900 }
901 }
902 /* translators: %d: Number of orders converted */
903 $this->response['message'] = sprintf( __( 'Successfully converted %d guest orders to registered users.', 'commercebird' ), $count );
904 $this->serve();
905 } /**
906 * Fetches webhooks from Zoho Inventory API.
907 *
908 * @return void
909 */
910 public function get_zoho_webhooks(): void {
911 $this->verify();
912 $zoho_inventory_oid = get_option( 'cmbird_zoho_inventory_oid' );
913 $zoho_inventory_url = get_option( 'cmbird_zoho_inventory_url' );
914 if ( ! $zoho_inventory_oid || ! $zoho_inventory_url ) {
915 $this->errors = array( 'message' => 'Missing Zoho organization ID or domain.' );
916 $this->serve();
917 return;
918 }
919 $url = $zoho_inventory_url . 'inventory/v1/settings/webhooks?page=1&per_page=50&usestate=false&organization_id=' . $zoho_inventory_oid;
920 try {
921 $execute_curl_call_handle = new CMBIRD_API_Handler_Zoho();
922 $json = $execute_curl_call_handle->execute_curl_call_get( $url );
923
924 // Check if the response is a WP_Error object.
925 if ( is_wp_error( $json ) ) {
926 $this->errors = array( 'message' => $json->get_error_message() );
927 } elseif ( isset( $json->webhooks ) && is_array( $json->webhooks ) ) {
928 $this->response = $json->webhooks;
929 } else {
930 $this->errors = array( 'message' => 'Failed to fetch webhooks or no webhooks found.' );
931 }
932 } catch ( Throwable $throwable ) {
933 $this->errors = array( 'message' => $throwable->getMessage() );
934 }
935 $this->serve();
936 }
937
938 /**
939 * Function to sync variable items from Zoho to WooCommerce.
940 */
941 public function sync_variable_items_from_zoho(): void {
942 $this->verify();
943
944 // Clear Orphan data.
945 $zi_common_class = new \CMBIRD_Common_Functions();
946 $zi_common_class->clear_orphan_data();
947
948 // check if a category is selected.
949 $selected_category = isset( $_GET['category'] ) ? sanitize_text_field( $_GET['category'] ) : null;
950
951 if ( $selected_category ) {
952 $categories = array( $selected_category ); // Only sync the selected category.
953 } else {
954 $zoho_item_category = get_option( 'cmbird_zoho_item_category' );
955 if ( $zoho_item_category ) {
956 // convert serialized string to array.
957 $categories = maybe_unserialize( $zoho_item_category );
958 if ( ! is_array( $categories ) ) {
959 $categories = array();
960 }
961 } else {
962 $categories = array();
963 }
964 }
965
966 if ( empty( $categories ) ) {
967 $this->errors = array( 'message' => __( 'Please select at least one category from cron tab', 'commercebird' ) );
968 } else {
969 foreach ( $categories as $category_index => $category_id ) {
970 $data = array(
971 'page' => 1,
972 'category' => $category_id,
973 );
974 $existing_schedule = as_has_scheduled_action( 'import_group_items_cron', $data );
975 // Schedule the action if it doesn't exist.
976 if ( ! $existing_schedule ) {
977 as_schedule_single_action( time(), 'import_group_items_cron', $data );
978 }
979 }
980 $this->response = array( 'message' => 'Items are being imported in background. You can visit other tabs :).' );
981 }
982 $this->serve();
983 }
984
985 /**
986 * Function to sync simple items from Zoho to WooCommerce.
987 */
988 public function sync_simple_items_from_zoho(): void {
989 $this->verify();
990
991 // Clear Orphan data.
992 $zi_common_class = new \CMBIRD_Common_Functions();
993 $zi_common_class->clear_orphan_data();
994
995 // Check if a category is selected.
996 // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Admin sync action, nonce verified in parent.
997 $selected_category = isset( $_GET['category'] ) ? sanitize_text_field( wp_unslash( $_GET['category'] ) ) : null;
998
999 if ( $selected_category ) {
1000 $categories = array( $selected_category ); // Only sync the selected category.
1001 } else {
1002 $zoho_item_category = get_option( 'cmbird_zoho_item_category' );
1003 if ( $zoho_item_category ) {
1004 // convert serialized string to array.
1005 $categories = maybe_unserialize( $zoho_item_category );
1006 if ( ! is_array( $categories ) ) {
1007 $categories = array();
1008 }
1009 } else {
1010 $categories = array();
1011 }
1012 }
1013
1014 if ( empty( $categories ) ) {
1015 $this->errors = array( 'message' => __( 'Please select at least one category from cron tab', 'commercebird' ) );
1016 } else {
1017 foreach ( $categories as $index => $category_id ) {
1018 $data = array(
1019 'page' => 1,
1020 'category' => $category_id,
1021 );
1022 // Add group parameter to prevent scheduling conflicts.
1023 $existing_schedule = as_has_scheduled_action( 'import_simple_items_cron', $data, 'commercebird' );
1024 if ( ! $existing_schedule ) {
1025 as_schedule_single_action( time(), 'import_simple_items_cron', $data, 'commercebird' );
1026 }
1027 }
1028 $this->response = array( 'message' => __( 'Items are being imported in background. You can visit other tabs :).', 'commercebird' ) );
1029 }
1030 $this->serve();
1031 }
1032
1033
1034 /**
1035 * Schedule a batch of WooCommerce product IDs for Zoho sync.
1036 *
1037 * Wrapped in a protected method to make testing easier.
1038 *
1039 * @param int[] $ids Array of post IDs to sync.
1040 * @return void
1041 */
1042 protected function schedule_zi_product( array $ids ): void {
1043 as_schedule_single_action( time(), 'sync_zi_product_cron', array( $ids ) );
1044 }
1045
1046 /**
1047 * Resolve the stored Zoho category IDs into WooCommerce product_cat term IDs.
1048 *
1049 * The option 'cmbird_zoho_item_category' is a plain serialised array of Zoho
1050 * category IDs. For each ID the corresponding WC term ID is resolved by
1051 * querying the per-term options stored as 'cmbird_zoho_id_for_term_id_{term_id}'.
1052 *
1053 * @param mixed $opt The raw option value returned by get_option().
1054 * @return int[] List of WC product_cat term IDs (empty array on failure).
1055 */
1056 private function normalize_category_list( $opt ): array {
1057 if ( empty( $opt ) ) {
1058 return array();
1059 }
1060
1061 $zoho_cat_ids = maybe_unserialize( $opt );
1062 if ( ! is_array( $zoho_cat_ids ) ) {
1063 return array();
1064 }
1065
1066 global $wpdb;
1067 $term_ids = array();
1068 foreach ( $zoho_cat_ids as $zoho_id ) {
1069 if ( empty( $zoho_id ) ) {
1070 continue;
1071 }
1072 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Need to find category mapping.
1073 $option_name = $wpdb->get_var(
1074 $wpdb->prepare(
1075 "SELECT option_name FROM $wpdb->options WHERE option_name LIKE %s AND option_value = %s LIMIT 1",
1076 'cmbird_zoho_id_for_term_id_%',
1077 (string) $zoho_id
1078 )
1079 );
1080 if ( ! empty( $option_name ) ) {
1081 $term_ids[] = (int) str_replace( 'cmbird_zoho_id_for_term_id_', '', $option_name );
1082 }
1083 }
1084
1085 return $term_ids;
1086 }
1087
1088 /**
1089 * Return all published products in the given categories which do not have a
1090 * Zoho item ID yet.
1091 *
1092 * @param int[] $categories Array of product_cat term IDs to filter by.
1093 * @return int[] List of post IDs.
1094 */
1095 protected function get_unsynced_product_ids( array $categories ): array {
1096 // make sure we actually have some categories to query against.
1097 if ( empty( $categories ) ) {
1098 return array();
1099 }
1100
1101 // cast everything to int just in case the caller passed strings.
1102 $categories = array_map( 'intval', $categories );
1103
1104 $args = array(
1105 'post_type' => 'product',
1106 'post_status' => 'publish',
1107 'fields' => 'ids',
1108 'posts_per_page' => -1,
1109 'tax_query' => array(
1110 array(
1111 'taxonomy' => 'product_cat',
1112 'field' => 'term_id',
1113 'terms' => $categories,
1114 ),
1115 ),
1116 // use a meta_query so the database only returns posts without the meta key.
1117 'meta_query' => array(
1118 array(
1119 'key' => 'zi_item_id',
1120 'compare' => 'NOT EXISTS',
1121 ),
1122 ),
1123 );
1124
1125 // WP_Query handles the heavy lifting; since we asked for IDs only we get a simple array.
1126 $query = new \WP_Query( $args );
1127
1128 return $query->posts ?: array();
1129 }
1130
1131 /**
1132 * Function to sync items from WooCommerce to Zoho.
1133 */
1134 public function sync_items_to_zoho(): void {
1135 $this->verify();
1136
1137 // figure out which categories should be considered for this sync; it can.
1138 // be passed explicitly via GET or fall back to the stored cron setting.
1139 $selected_category = isset( $_GET['category'] ) ? sanitize_text_field( wp_unslash( $_GET['category'] ) ) : null;
1140
1141 if ( $selected_category ) {
1142 $categories = array( $selected_category );
1143 } else {
1144 $categories = $this->normalize_category_list( get_option( 'cmbird_zoho_item_category' ) );
1145 }
1146
1147 if ( empty( $categories ) ) {
1148 $this->errors = array( 'message' => __( 'Please select at least one category from category page', 'commercebird' ) );
1149 $this->serve();
1150 return;
1151 }
1152
1153 // get all unsynced products within those categories.
1154 $post_ids = $this->get_unsynced_product_ids( $categories );
1155
1156 if ( empty( $post_ids ) ) {
1157 $this->response = array( 'message' => 'No items to sync.' );
1158 $this->serve();
1159 return;
1160 } // get all unsynced products within those categories.
1161 $post_ids = $this->get_unsynced_product_ids( $categories );
1162
1163 if ( empty( $post_ids ) ) {
1164 $this->response = array( 'message' => 'No items to sync.' );
1165 $this->serve();
1166 return;
1167 }
1168
1169 $product_ids = array();
1170 foreach ( $post_ids as $post_id ) {
1171 $product_ids[] = $post_id;
1172 if ( count( $product_ids ) === 10 ) {
1173 $this->schedule_zi_product( $product_ids );
1174 $product_ids = array();
1175 }
1176 }
1177
1178 if ( count( $product_ids ) > 0 ) {
1179 $this->schedule_zi_product( $product_ids );
1180 }
1181
1182 $this->response = array( 'message' => 'Products are being synced in background.' );
1183 $this->serve();
1184 }
1185
1186 /**
1187 * Sync contacts from Zoho to WooCommerce.
1188 */
1189 public function sync_contacts_from_zoho(): void {
1190 $this->verify();
1191
1192 $page = 1;
1193 $data_arr = (object) array();
1194 $data_arr->page = $page;
1195 $existing_schedule = as_has_scheduled_action( 'sync_zi_import_contacts', array( $data_arr ) );
1196
1197 // Wrap this via Action Scheduler per page.
1198 if ( ! $existing_schedule ) {
1199 // Schedule the cron job.
1200 as_schedule_single_action( time(), 'sync_zi_import_contacts', array( $data_arr ) );
1201 }
1202
1203 $this->response = array( 'message' => 'Syncing in background. You can visit other tabs :).' );
1204 $this->serve();
1205 }
1206
1207 /**
1208 * Sync composite items from Zoho to WooCommerce.
1209 */
1210 public function sync_composite_items_from_zoho(): void {
1211 $this->verify();
1212
1213 // Clear Orphan data.
1214 $zi_common_class = new \CMBIRD_Common_Functions();
1215 $zi_common_class->clear_orphan_data();
1216
1217 // check if a category is selected.
1218 $selected_category = isset( $_GET['category'] ) ? sanitize_text_field( $_GET['category'] ) : null;
1219 if ( $selected_category ) {
1220 $categories = array( $selected_category ); // Only sync the selected category.
1221 } else {
1222 $zoho_item_category = get_option( 'cmbird_zoho_item_category' );
1223 if ( $zoho_item_category ) {
1224 // convert serialized string to array.
1225 $categories = maybe_unserialize( $zoho_item_category );
1226 if ( ! is_array( $categories ) ) {
1227 $categories = array();
1228 }
1229 } else {
1230 $categories = array();
1231 }
1232 }
1233
1234 $item_add_resp = array();
1235 foreach ( $categories as $category_id ) {
1236 $product_class = new \CMBIRD_Products_ZI();
1237 $response = $product_class->recursively_sync_composite_item_from_zoho( 1, $category_id );
1238 $item_add_resp = array_merge( $item_add_resp, $response );
1239 }
1240
1241 // Send log message to admin if there are responses.
1242 if ( ! empty( $item_add_resp ) ) {
1243 $this->send_log_message_to_admin( $item_add_resp, 'Log Message for manual sync', 'Composite item sync from zoho' );
1244 }
1245
1246 $this->response = $item_add_resp;
1247 $this->serve();
1248 }
1249
1250 /**
1251 * Sync composite items from WooCommerce to Zoho.
1252 */
1253 public function sync_composite_items_to_zoho(): void {
1254 $this->verify();
1255
1256 $irgs = array(
1257 'post_type' => array( 'product' ),
1258 'posts_per_page' => '-1',
1259 'post_status' => 'publish',
1260 'tax_query' => array(
1261 array(
1262 'taxonomy' => 'product_type',
1263 'field' => 'slug',
1264 'terms' => 'bundle',
1265 ),
1266 ),
1267 );
1268
1269 $my_query = new \WP_Query( $irgs );
1270 $bundled_product = $my_query->posts;
1271 if ( count( $bundled_product ) > 0 ) {
1272 foreach ( $bundled_product as $prod ) {
1273 $bundle_childs = \WC_PB_DB::query_bundled_items(
1274 array(
1275 'return' => 'id=>product_id', // 'objects'.
1276 'bundle_id' => array( $prod->ID ),
1277 )
1278 );
1279 if ( count( $bundle_childs ) > 0 ) {
1280 foreach ( $bundle_childs as $child_item_id ) {
1281 $zoho_item_id = get_post_meta( $child_item_id, 'zi_item_id', true );
1282 if ( empty( $zoho_item_id ) ) {
1283 break;
1284 }
1285 }
1286 }
1287 }
1288 }
1289
1290 $this->response = array(
1291 'message' => 'Sync Started',
1292 'code' => 200,
1293 );
1294 $this->serve();
1295 }
1296
1297 /**
1298 * Send log message to admin.
1299 *
1300 * @param array $sync_logs Log messages.
1301 * @param string $subject Email subject.
1302 * @param string $message Email message.
1303 */
1304 private function send_log_message_to_admin( $sync_logs, $subject, $message ): void {
1305 $table_root = "<h3>$message</h3>";
1306 $table_root .= '<table><thead><tr><th>Action</th><th> Log message</th></tr></thead><tbody>';
1307
1308 foreach ( $sync_logs as $logs ) {
1309 $table_root .= "<tr><td>$logs->resp_id</td><td>$logs->message</td></tr>";
1310 }
1311 $table_root .= '</tbody></table>';
1312 $common_class = new \CMBIRD_Common_Functions();
1313 $common_class->send_email( $subject, $table_root );
1314 }
1315 }
1316