PluginProbe ʕ •ᴥ•ʔ
CommerceBird – AI Command Center, ERP Integrations & B2B for WooCommerce (Zoho, Exact Online). / 3.0.1
CommerceBird – AI Command Center, ERP Integrations & B2B for WooCommerce (Zoho, Exact Online). v3.0.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 / admin / includes / Actions / Ajax / ZohoInventoryAjax.php
commercebird / admin / includes / Actions / Ajax Last commit date
AcfAjax.php 3 weeks ago ExactOnlineAjax.php 1 month ago SettingsAjax.php 1 month ago ZohoCRMAjax.php 4 months ago ZohoInventoryAjax.php 1 month ago index.php 1 year ago
ZohoInventoryAjax.php
1651 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_custom_fields' => 'zoho_custom_fields_collect',
96 'get_zoho_webhooks' => 'get_zoho_webhooks',
97 'handle_code' => 'handle_code',
98 'convert_guest' => 'convert_guest',
99 'zoho_ajax_call_variable_item_from_zoho' => 'sync_variable_items_from_zoho',
100 'zoho_ajax_call_item_from_zoho' => 'sync_simple_items_from_zoho',
101 'zoho_ajax_call_item' => 'sync_items_to_zoho',
102 'import_zoho_contacts' => 'sync_contacts_from_zoho',
103 'zoho_ajax_call_composite_item_from_zoho' => 'sync_composite_items_from_zoho',
104 'zoho_ajax_call_composite_item' => 'sync_composite_items_to_zoho',
105 'get_category_mappings' => 'category_mappings_get',
106 'get_category_review_queue' => 'category_review_queue_get',
107 'set_manual_category_mapping' => 'category_mapping_set_manual',
108 'resolve_category_review' => 'category_review_resolve',
109 'delete_category_mapping' => 'category_mapping_delete',
110 );
111
112 private const SOURCE = 'zoho';
113
114 /**
115 * ZohoInventoryAjax constructor.
116 */
117 public function __construct() {
118 $this->load_actions();
119 }
120
121 /**
122 * Collects custom checkout fields for WooCommerce.
123 *
124 * This function verifies the request and collects custom fields from the
125 * THWCFD_Utils::get_checkout_fields() function if the THWCFD class exists.
126 *
127 * @return void
128 */
129 public function wc_custom_fields_collect(): void {
130 $this->verify();
131 $types = array( 'billing', 'shipping', 'additional' );
132 $all_fields = array();
133 $custom_fields = array();
134 // Get all the fields.
135 foreach ( $types as $type ) {
136 // Skip if an unsupported type.
137 if (
138 ! in_array(
139 $type,
140 array(
141 'billing',
142 'shipping',
143 'additional',
144 ),
145 true,
146 )
147 ) {
148 continue;
149 }
150
151 $temp_fields = get_option( 'wc_fields_' . $type );
152 if ( false !== $temp_fields ) {
153 $all_fields = array_merge( $all_fields, $temp_fields );
154 }
155 }
156 // Loop through each field to see if it is a custom field.
157 foreach ( $all_fields as $name => $options ) {
158 if ( isset( $options['custom'] ) && $options['custom'] ) {
159 $label = trim( $options['label'] );
160 $custom_fields[ $name ] = empty( $label ) ? __( 'Please set label', 'commercebird' ) : $label;
161 }
162 }
163
164 $this->response = $custom_fields;
165
166 $this->serve();
167 }
168
169 /**
170 * Collects custom Zoho Inventory fields for sales orders or items.
171 *
172 * @return void
173 */
174 public function zoho_custom_fields_collect(): void {
175 $this->verify();
176
177 $entity = isset( $this->request['entity'] ) ? sanitize_key( $this->request['entity'] ) : 'salesorder';
178 if ( ! in_array( $entity, array( 'salesorder', 'item' ), true ) ) {
179 $this->errors = array( 'message' => 'Invalid entity type' );
180 $this->serve();
181 return;
182 }
183
184 $zoho_inventory_oid = get_option( 'cmbird_zoho_inventory_oid' );
185 $zoho_inventory_url = get_option( 'cmbird_zoho_inventory_url' );
186
187 $url = $zoho_inventory_url . 'inventory/v1/settings/fields?entity=' . $entity . '&organization_id=' . $zoho_inventory_oid;
188
189 $execute_curl_call_handle = new CMBIRD_API_Handler_Zoho();
190 $zoho_fields = array();
191 $debug = array(
192 'urls_tried' => array(),
193 'url_succeeded' => null,
194 'response_path' => null,
195 'raw_code' => null,
196 );
197
198 $json = $execute_curl_call_handle->execute_curl_call_get( $url );
199 $debug_entry = array(
200 'url' => $url,
201 'wp_error' => false,
202 'code' => null,
203 );
204
205 if ( is_wp_error( $json ) ) {
206 $debug_entry['wp_error'] = true;
207 $debug_entry['matched'] = 'none';
208 $debug['urls_tried'][] = $debug_entry;
209 } else {
210 $debug_entry['code'] = isset( $json->code ) ? $json->code : null;
211 $debug['raw_code'] = $debug_entry['code'];
212
213 if ( isset( $json->fields ) && is_array( $json->fields ) ) {
214 $zoho_fields = $json->fields;
215 $debug_entry['matched'] = 'json->fields';
216 $debug['url_succeeded'] = $url;
217 $debug['response_path'] = 'json->fields';
218 } elseif ( isset( $json->data->fields ) && is_array( $json->data->fields ) ) {
219 $zoho_fields = $json->data->fields;
220 $debug_entry['matched'] = 'json->data->fields';
221 $debug['url_succeeded'] = $url;
222 $debug['response_path'] = 'json->data->fields';
223 } else {
224 $debug_entry['matched'] = 'none';
225 }
226
227 $debug['urls_tried'][] = $debug_entry;
228 }
229
230 $fields = array();
231 if ( ! empty( $zoho_fields ) ) {
232 foreach ( $zoho_fields as $field ) {
233 $fields[] = array(
234 'field_name' => isset( $field->field_name ) ? (string) $field->field_name : '',
235 'field_name_formatted' => isset( $field->field_name_formatted ) ? (string) $field->field_name_formatted : '',
236 'data_type' => isset( $field->data_type ) ? (string) $field->data_type : '',
237 'is_custom_field' => ! empty( $field->is_custom_field ),
238 );
239 }
240 }
241
242 $this->response = array(
243 'fields' => $fields,
244 '_debug' => $debug,
245 );
246 $this->serve();
247 }
248
249 /**
250 * Retrieves the connection details.
251 *
252 * @return void
253 */
254 public function connection_get(): void {
255 $this->verify();
256 $this->response['account_domain'] = get_option( 'cmbird_zoho_inventory_domain', '' );
257 $this->response['organization_id'] = get_option( 'cmbird_zoho_inventory_oid', '' );
258 $this->response['client_id'] = get_option( 'cmbird_zoho_inventory_cid', '' );
259 $this->response['client_secret'] = get_option( 'cmbird_zoho_inventory_cs', '' );
260 $this->response['inventory_url'] = get_option( 'cmbird_zoho_inventory_url', '' );
261 $this->response['redirect_uri'] = get_option( 'cmbird_authorization_redirect_uri', '' );
262 $this->serve();
263 }
264
265 /**
266 * Retrieves the fields and serves the response.
267 *
268 * @return void
269 */
270 public function fields_get(): void {
271 $this->verify();
272
273 $entity = isset( $this->request['entity'] ) ? sanitize_key( $this->request['entity'] ) : 'salesorder';
274 if ( ! in_array( $entity, array( 'salesorder', 'item' ), true ) ) {
275 $entity = 'salesorder';
276 }
277
278 $stored_option = get_option( 'cmbird_wootozoho_custom_fields', array() );
279 if ( is_string( $stored_option ) ) {
280 $decoded = json_decode( $stored_option, true );
281 if ( is_array( $decoded ) ) {
282 $stored_option = $decoded;
283 }
284 }
285
286 if ( is_array( $stored_option ) && ( isset( $stored_option['salesorder'] ) || isset( $stored_option['item'] ) ) ) {
287 $this->response['form'] = isset( $stored_option[ $entity ] ) && is_array( $stored_option[ $entity ] ) ? $stored_option[ $entity ] : array();
288 } else {
289 // Backward compatibility: legacy flat mapping belongs to product/item mapping.
290 $this->response['form'] = 'item' === $entity && is_array( $stored_option ) ? $stored_option : array();
291 }
292
293 $this->serve();
294 }
295
296 /**
297 * Sets the custom fields for the form.
298 *
299 * @return void
300 */
301 public function fields_set(): void {
302 $this->verify( array( 'form', 'entity' ) );
303 try {
304 $entity = isset( $this->data['entity'] ) ? sanitize_key( (string) $this->data['entity'] ) : '';
305 if ( empty( $entity ) && isset( $this->request['entity'] ) ) {
306 $entity = sanitize_key( $this->request['entity'] );
307 }
308 if ( empty( $entity ) ) {
309 $entity = 'salesorder';
310 }
311 if ( ! in_array( $entity, array( 'salesorder', 'item' ), true ) ) {
312 $entity = 'salesorder';
313 }
314
315 $form_data = json_decode( (string) $this->data['form'], true );
316 if ( ! is_array( $form_data ) ) {
317 $form_data = array();
318 }
319
320 $stored_option = get_option( 'cmbird_wootozoho_custom_fields', array() );
321 if ( is_string( $stored_option ) ) {
322 $decoded = json_decode( $stored_option, true );
323 if ( is_array( $decoded ) ) {
324 $stored_option = $decoded;
325 }
326 }
327
328 if ( ! is_array( $stored_option ) || ( ! isset( $stored_option['salesorder'] ) && ! isset( $stored_option['item'] ) ) ) {
329 $stored_option = array(
330 'salesorder' => array(),
331 'item' => is_array( $stored_option ) ? $stored_option : array(),
332 );
333 }
334
335 $stored_option[ $entity ] = $form_data;
336 update_option( 'cmbird_wootozoho_custom_fields', wp_json_encode( $stored_option ) );
337 $this->response = array( 'message' => 'saved' );
338 } catch ( Throwable $throwable ) {
339 $this->errors = array( 'message' => $throwable->getMessage() );
340 }
341 $this->serve();
342 }
343
344 /**
345 * Resets the fields.
346 *
347 * @return void
348 */
349 public function fields_reset(): void {
350 $this->verify();
351
352 $entity = isset( $this->data['entity'] ) ? sanitize_key( (string) $this->data['entity'] ) : '';
353 if ( empty( $entity ) && isset( $this->request['entity'] ) ) {
354 $entity = sanitize_key( $this->request['entity'] );
355 }
356 if ( in_array( $entity, array( 'salesorder', 'item' ), true ) ) {
357 $stored_option = get_option( 'cmbird_wootozoho_custom_fields', array() );
358 if ( is_string( $stored_option ) ) {
359 $decoded = json_decode( $stored_option, true );
360 if ( is_array( $decoded ) ) {
361 $stored_option = $decoded;
362 }
363 }
364
365 if ( is_array( $stored_option ) && ( isset( $stored_option['salesorder'] ) || isset( $stored_option['item'] ) ) ) {
366 $stored_option[ $entity ] = array();
367 update_option( 'cmbird_wootozoho_custom_fields', wp_json_encode( $stored_option ) );
368 } elseif ( 'salesorder' === $entity ) {
369 delete_option( 'cmbird_wootozoho_custom_fields' );
370 }
371 } else {
372 delete_option( 'cmbird_wootozoho_custom_fields' );
373 }
374
375 $this->response = array( 'message' => 'Reset successfully!' );
376 $this->serve();
377 }
378
379 /**
380 * Resets the price.
381 *
382 * @return void
383 */
384 public function price_reset(): void {
385 $this->verify();
386 delete_option( 'cmbird_zoho_pricelist_id' );
387 delete_option( 'cmbird_zoho_pricelist_role' );
388 delete_option( 'cmbird_zoho_pricelist_wcb2b_groups' );
389 $this->response = array( 'message' => 'Reset successfully!' );
390 $this->serve();
391 }
392
393 /**
394 * Retrieves the price information.
395 *
396 * @return void
397 */
398 public function price_get(): void {
399 $this->verify();
400 $this->response['zoho_inventory_pricelist'] = get_option( 'cmbird_zoho_pricelist_id' );
401 $this->response['wp_user_role'] = get_option( 'cmbird_zoho_pricelist_role' );
402 if ( \CMBIRD_Common_Functions::is_wcb2b_active() ) {
403 $this->response['wcb2b'] = get_option( 'cmbird_zoho_pricelist_wcb2b_groups' );
404 }
405 $this->serve();
406 }
407
408 /**
409 * Sets the price and saves the pricelist.
410 */
411 public function price_set(): void {
412 $this->verify( self::FORMS['price'] );
413 try {
414 $import_pricelist = new CMBIRD_Pricelist_ZI();
415 $success = $import_pricelist->save_price_list( $this->data );
416 if ( class_exists( 'Addify_B2B_Plugin' ) ) {
417 update_option( 'cmbird_zoho_pricelist_role', $this->data['wp_user_role'] );
418 } else {
419 update_option( 'cmbird_zoho_pricelist_wcb2b_groups', $this->data['wcb2b'] );
420 }
421 if ( $success ) {
422 $this->response = array( 'message' => 'Saved' );
423 if ( \CMBIRD_Common_Functions::is_wcb2b_active() ) {
424 $this->response['wcb2b'] = $this->data['wcb2b'];
425 }
426 } else {
427 $this->errors = array( 'message' => 'Failed to save' );
428 }
429 } catch ( Throwable $throwable ) {
430 $this->errors = array( 'message' => $throwable->getMessage() );
431 $this->write_log(
432 array(
433 'message' => $throwable->getMessage(),
434 'files' => wp_list_pluck( $throwable->getTrace(), 'file', 'line' ),
435 ),
436 'zoho-ajax-error'
437 );
438 }
439
440 $this->serve();
441 }
442
443 /**
444 * Collects Zoho prices.
445 *
446 * @return void
447 */
448 public function zoho_prices_collect(): void {
449 $this->verify();
450 $price_list_class = new CMBIRD_Pricelist_ZI();
451 $prices = $price_list_class->zi_get_all_pricelist();
452 $this->response = wp_list_pluck( $prices, 'name', 'pricebook_id' );
453 $this->serve();
454 }
455
456 /**
457 * Retrieves the contact details.
458 *
459 * @return void
460 */
461 public function contact_get() {
462 $this->verify();
463 $this->response = $this->option_status_get( self::FORMS['contact'], self::SOURCE );
464 $this->serve();
465 }
466 /**
467 * Sets the contact and updates the status.
468 *
469 * @return void
470 */
471 public function contact_set() {
472 $this->verify( self::FORMS['contact'] );
473 $this->option_status_update( $this->data, self::SOURCE );
474 if ( isset( $this->data['enable'] ) && ! empty( $this->data['enable'] ) ) {
475 if ( ! wp_next_scheduled( 'zoho_contact_sync' ) ) {
476 wp_schedule_event( time(), 'daily', 'zoho_contact_sync' );
477 }
478 } else {
479 wp_clear_scheduled_hook( 'zoho_contact_sync' );
480 }
481 $this->response = array( 'message' => 'Saved!' );
482 $this->serve();
483 }
484 /**
485 * Resets the contact.
486 *
487 * @return void
488 */
489 public function contact_reset() {
490 $this->verify();
491 $this->option_status_remove( self::FORMS['contact'], self::SOURCE );
492 $this->response = array( 'message' => 'Reset successfully!' );
493 $this->serve();
494 }
495
496 /**
497 * Sets the order and updates the status.
498 *
499 * @return void
500 */
501 public function order_set(): void {
502 $this->verify( self::FORMS['order'] );
503 $this->option_status_update( $this->data, self::SOURCE );
504 $this->response = array( 'message' => 'Saved!' );
505 $this->serve();
506 }
507
508 /**
509 * Retrieves the order.
510 *
511 * @return void
512 */
513 public function order_get(): void {
514 $this->verify();
515 $this->response = $this->option_status_get( self::FORMS['order'], self::SOURCE );
516 // option_status_get defaults missing options to ''. Ensure location_country_map.
517 // is always returned as an object so the frontend can rely on a consistent type.
518 if ( ! is_array( $this->response['location_country_map'] ) ) {
519 $this->response['location_country_map'] = new \stdClass();
520 }
521 $this->serve();
522 }
523
524 /**
525 * Resets the order.
526 *
527 * @return void
528 */
529 public function order_reset(): void {
530 $this->verify();
531 $this->option_status_remove( self::FORMS['order'], self::SOURCE );
532 $this->response = array( 'message' => 'Reset successfully!' );
533 $this->serve();
534 }
535
536 /**
537 * Collects Zoho locations.
538 *
539 * This function verifies the request, retrieves the Zoho inventory
540 * organization ID and URL from the options, constructs the URL
541 * for the API call, and executes the cURL call to retrieve the
542 * locations. The response is then processed to extract the
543 * location names and IDs, and stored in the class property.
544 *
545 * @return void
546 */
547 public function zoho_locations_collect(): void {
548 // $fd = fopen( __DIR__ . '/zoho_locations.txt', 'w+' );
549 $this->verify();
550 $zoho_inventory_oid = get_option( 'cmbird_zoho_inventory_oid' );
551 $zoho_inventory_url = get_option( 'cmbird_zoho_inventory_url' );
552 $url = $zoho_inventory_url . 'inventory/v1/locations?organization_id=' . $zoho_inventory_oid;
553 $execute_curl_call_handle = new CMBIRD_API_Handler_Zoho();
554 $json = $execute_curl_call_handle->execute_curl_call_get( $url );
555 // save the first location id to the database of the first object of json.
556 if ( ! empty( $json->locations ) && is_array( $json->locations ) && array_key_exists( 0, $json->locations ) ) {
557 $location_id = $json->locations[0]->location_id;
558 update_option( 'cmbird_zoho_parent_location_id_status', $location_id );
559 }
560 $this->response = wp_list_pluck( $json->locations, 'location_name', 'location_id' );
561 $this->serve();
562 }
563
564 /**
565 * Sets the cron for the class.
566 */
567 public function cron_set(): void {
568 $this->verify( self::FORMS['cron'] );
569 if ( array_key_exists( 'form', $this->data ) ) {
570 $decode = json_decode( $this->data['form'], true );
571 update_option( 'zi_cron_interval', $decode['zi_cron_interval'] );
572 unset( $decode['zi_cron_interval'] );
573 $this->option_status_update( $decode, self::SOURCE );
574 }
575 if ( array_key_exists( 'categories', $this->data ) ) {
576 $decode = json_decode( $this->data['categories'] );
577 update_option( 'cmbird_zoho_item_category', serialize( $decode ) );
578 }
579 $this->response = array( 'message' => 'Saved' );
580 $this->serve();
581 }
582
583 /**
584 * Executes the cron_get function.
585 *
586 * This function verifies the form status and retrieves the form options
587 * for syncing name, price, image, and description. It also retrieves
588 * the cron interval and the Zoho item categories.
589 *
590 * @return void
591 */
592 public function cron_get(): void {
593 $this->verify();
594 $this->response = array();
595 $this->response['form'] = $this->option_status_get(
596 array(
597 'disable_name_sync',
598 'disable_price_sync',
599 'disable_image_sync',
600 'disable_description_sync',
601 ),
602 self::SOURCE,
603 );
604 $this->response['form']['zi_cron_interval'] = get_option( 'zi_cron_interval', 'none' );
605 $categories = get_option( 'cmbird_zoho_item_category', '' );
606 $selected_zoho_ids = maybe_unserialize( $categories );
607 if ( ! is_array( $selected_zoho_ids ) ) {
608 $selected_zoho_ids = array();
609 }
610
611 $map = new \CommerceBird\Admin\Mappings\ZohoCategoryMap();
612 $resolved_term_ids = array();
613 $unresolved_zoho_ids = array();
614
615 foreach ( $selected_zoho_ids as $zoho_id ) {
616 $normalized_zoho_id = (string) $zoho_id;
617 if ( '' === $normalized_zoho_id ) {
618 continue;
619 }
620
621 $term_id = $map->term_id_for_zoho_id_any_status( $normalized_zoho_id );
622 if ( null === $term_id ) {
623 $unresolved_zoho_ids[] = $normalized_zoho_id;
624 continue;
625 }
626
627 $resolved_term_ids[] = $term_id;
628 }
629
630 $this->response['categories'] = array_values( array_unique( array_map( 'strval', $selected_zoho_ids ) ) );
631 $this->response['category_selection'] = array(
632 'resolved_term_ids' => array_values( array_unique( array_map( 'intval', $resolved_term_ids ) ) ),
633 'unresolved_zoho_ids' => array_values( array_unique( $unresolved_zoho_ids ) ),
634 );
635 $this->serve();
636 }
637
638 /**
639 * Resets the cron job.
640 *
641 * This function verifies the cron job, removes the specified options,
642 * deletes the 'zoho_item_category' option, and serves the cron job.
643 *
644 * @return void
645 */
646 public function cron_reset(): void {
647 $this->verify();
648 $this->option_status_remove(
649 array(
650 'disable_name_sync',
651 'disable_price_sync',
652 'disable_image_sync',
653 'disable_description_sync',
654 ),
655 self::SOURCE,
656 );
657 delete_option( 'cmbird_zoho_item_category' );
658 // Delete category mappings from the new table.
659 $map = new \CommerceBird\Admin\Mappings\ZohoCategoryMap();
660 $count_before = count( $map->list( 0, PHP_INT_MAX, 'active' ) );
661 $count_review = count( $map->list( 0, PHP_INT_MAX, 'review' ) );
662 $count_orphan = count( $map->list( 0, PHP_INT_MAX, 'orphan' ) );
663
664 global $wpdb;
665 $deleted_rows = (int) $wpdb->query(
666 "DELETE FROM {$wpdb->prefix}cmbird_zi_category_map"
667 );
668
669 $total = $count_before + $count_review + $count_orphan;
670 $this->response = array( 'message' => "Reset successfully! Found {$total} category mapping rows, deleted {$deleted_rows}." );
671 $this->serve();
672 }
673
674 /**
675 * Collects all categories from both Zoho Inventory and WooCommerce.
676 *
677 * This function retrieves categories from Zoho Inventory and merges them with
678 * WooCommerce categories to provide a complete list for the frontend display.
679 * This ensures all 66+ categories are visible, not just the ones in Zoho.
680 */
681 public function zoho_categories_collect(): void {
682 $this->verify();
683 $category_class = new CMBIRD_Categories_ZI();
684
685 // Only return Zoho categories since we're storing Zoho category IDs in options.
686 $zoho_categories = $category_class->cmbird_get_zoho_item_categories();
687 if ( gettype( $zoho_categories ) === 'array' ) {
688 $this->response = $zoho_categories;
689 } else {
690 $this->response = array();
691 }
692 $this->serve();
693 }
694
695 /**
696 * Returns paginated active category mappings, enriched with WC term + Zoho category names.
697 */
698 public function category_mappings_get(): void {
699 $this->verify();
700
701 $map = new \CommerceBird\Admin\Mappings\ZohoCategoryMap();
702 $rows = $map->list( 0, 500, 'all' );
703
704 $zoho_categories = ( new \CMBIRD_Categories_ZI() )->cmbird_get_zoho_item_categories();
705 $zoho_lookup = array();
706 if ( is_array( $zoho_categories ) ) {
707 foreach ( $zoho_categories as $z ) {
708 $zoho_lookup[ (string) $z['category_id'] ] = (string) ( $z['category_name'] ?? '' );
709 }
710 }
711
712 $enriched = array();
713 foreach ( $rows as $row ) {
714 $term = get_term( (int) $row['wc_term_id'], 'product_cat' );
715 $term_name = ( $term && ! is_wp_error( $term ) ) ? $term->name : sprintf( '(deleted term #%d)', $row['wc_term_id'] );
716 $enriched[] = array(
717 'row_id' => (int) $row['id'],
718 'wc_term_id' => (int) $row['wc_term_id'],
719 'wc_term_name' => $term_name,
720 'zoho_category_id' => (string) $row['zoho_category_id'],
721 'zoho_category_name' => $zoho_lookup[ (string) $row['zoho_category_id'] ] ?? '(unknown)',
722 'source' => (string) $row['source'],
723 'status' => (string) $row['status'],
724 'review_reason' => (string) ( $row['review_reason'] ?? '' ),
725 'last_verified_at' => $row['last_verified_at'],
726 );
727 }
728
729 $this->response = $enriched;
730 $this->serve();
731 }
732
733 /**
734 * Returns review and orphan rows merged for operator triage.
735 */
736 public function category_review_queue_get(): void {
737 $this->verify();
738
739 $map = new \CommerceBird\Admin\Mappings\ZohoCategoryMap();
740 $rows = array_merge(
741 $map->list( 0, 200, 'review' ),
742 $map->list( 0, 200, 'orphan' )
743 );
744
745 $zoho_categories = ( new \CMBIRD_Categories_ZI() )->cmbird_get_zoho_item_categories();
746 $zoho_lookup = array();
747 if ( is_array( $zoho_categories ) ) {
748 foreach ( $zoho_categories as $z ) {
749 $zoho_lookup[ (string) $z['category_id'] ] = (string) ( $z['category_name'] ?? '' );
750 }
751 }
752
753 $enriched = array();
754 foreach ( $rows as $row ) {
755 $term = get_term( (int) $row['wc_term_id'], 'product_cat' );
756 $term_name = ( $term && ! is_wp_error( $term ) ) ? $term->name : sprintf( '(deleted term #%d)', $row['wc_term_id'] );
757
758 // Find the active conflict, if any.
759 global $wpdb;
760 $conflict = $wpdb->get_row(
761 $wpdb->prepare(
762 "SELECT wc_term_id, zoho_category_id FROM {$wpdb->prefix}cmbird_zi_category_map
763 WHERE status = 'active'
764 AND ( wc_term_id = %d OR zoho_category_id = %s )
765 LIMIT 1",
766 (int) $row['wc_term_id'],
767 (string) $row['zoho_category_id']
768 ),
769 ARRAY_A
770 );
771
772 $enriched[] = array(
773 'row_id' => (int) $row['id'],
774 'wc_term_id' => (int) $row['wc_term_id'],
775 'wc_term_name' => $term_name,
776 'zoho_category_id' => (string) $row['zoho_category_id'],
777 'zoho_category_name' => $zoho_lookup[ (string) $row['zoho_category_id'] ] ?? '(unknown)',
778 'source' => (string) $row['source'],
779 'status' => (string) $row['status'],
780 'last_verified_at' => $row['last_verified_at'],
781 'review_reason' => (string) $row['review_reason'],
782 'conflicts_with' => $conflict ?: null,
783 );
784 }
785
786 $this->response = $enriched;
787 $this->serve();
788 }
789
790 /**
791 * Delete a single category mapping row by its ID.
792 */
793 public function category_mapping_delete(): void {
794 $this->verify( array( 'row_id' ) );
795
796 $row_id = (int) $this->data['row_id'];
797 $deleted = ( new \CommerceBird\Admin\Mappings\ZohoCategoryMap() )->delete_by_id( $row_id );
798
799 if ( $deleted ) {
800 $this->response = array( 'message' => 'Mapping removed' );
801 } else {
802 $this->errors = array( 'message' => 'Mapping not found or already removed' );
803 }
804 $this->serve();
805 }
806
807 /**
808 * Create or update a manual category mapping.
809 */
810 public function category_mapping_set_manual(): void {
811 $this->verify( array( 'wc_term_id', 'zoho_category_id' ) );
812
813 $map = new \CommerceBird\Admin\Mappings\ZohoCategoryMap();
814 $result = $map->set_manual(
815 (int) $this->data['wc_term_id'],
816 (string) $this->data['zoho_category_id']
817 );
818
819 $this->response = array(
820 'message' => 'Manual mapping saved',
821 'status' => $result->status,
822 'row_id' => $result->row_id,
823 );
824 $this->serve();
825 }
826
827 /**
828 * Resolve a category mapping review.
829 */
830 public function category_review_resolve(): void {
831 $this->verify( array( 'row_id', 'resolution' ) );
832
833 $resolution = match ( (string) $this->data['resolution'] ) {
834 'keep_existing' => \CommerceBird\Admin\Mappings\ReviewResolution::KeepExisting,
835 'replace_with_pending' => \CommerceBird\Admin\Mappings\ReviewResolution::ReplaceWithPending,
836 'discard_both' => \CommerceBird\Admin\Mappings\ReviewResolution::DiscardBoth,
837 default => null,
838 };
839
840 if ( null === $resolution ) {
841 $this->errors = array( 'message' => 'Invalid resolution' );
842 $this->serve();
843 return;
844 }
845
846 ( new \CommerceBird\Admin\Mappings\ZohoCategoryMap() )->resolve_review(
847 (int) $this->data['row_id'],
848 $resolution
849 );
850
851 $this->response = array( 'message' => 'Resolved' );
852 $this->serve();
853 }
854
855 /**
856 * Executes the connection process and handles the response.
857 */
858 public function connection_done(): void {
859 $this->verify();
860 $zoho_inventory_url = get_option( 'cmbird_zoho_inventory_url' );
861 $zoho_inventory_oid = get_option( 'cmbird_zoho_inventory_oid' );
862 $url = $zoho_inventory_url . 'inventory/v1/apiusagereport/dashboard?version=all&organization_id=' . $zoho_inventory_oid;
863
864 $execute_curl_call_handle = new CMBIRD_API_Handler_Zoho();
865 $json = $execute_curl_call_handle->execute_curl_call_get( $url );
866
867 if ( is_wp_error( $json ) ) {
868 $this->errors = array( 'message' => $json->get_error_message() );
869 return;
870 }
871
872 if ( empty( $json ) || empty( $json->data ) || empty( $json->data->api_usage ) ) {
873 $this->errors = array( 'message' => 'We lost connection with Zoho. Please refresh the page.' );
874 return;
875 }
876 $api_usage = $json->data->api_usage;
877 // Debug JSON encoding.
878 $encoded_api_usage = wp_json_encode( $api_usage );
879 // Ensure it's not null.
880 $this->response = json_decode( $encoded_api_usage, true ) ?? array();
881 $this->serve();
882 }
883
884 /**
885 * Executes the product_set function.
886 */
887 public function product_set(): void {
888 $this->verify( self::FORMS['product'] );
889 if ( ! empty( $this->data ) ) {
890 $this->option_status_update( $this->data, self::SOURCE );
891 }
892 $this->response = array( 'message' => 'Saved!' );
893 $this->serve();
894 }
895
896 /**
897 * Retrieves the product data.
898 *
899 * @return void
900 */
901 public function product_get(): void {
902 $this->verify();
903 $this->response = $this->option_status_get( self::FORMS['product'], self::SOURCE );
904 $this->serve();
905 }
906
907 /**
908 * Resets the product.
909 *
910 * @return void
911 */
912 public function product_reset(): void {
913 $this->verify();
914 $this->option_status_remove( self::FORMS['product'], self::SOURCE );
915 $this->response = array( 'message' => 'Reset successfully!' );
916 $this->serve();
917 }
918
919 /**
920 * Retrieves the tax information.
921 *
922 * @return void
923 */
924 public function tax_get(): void {
925 $this->verify();
926 $this->response = array();
927 foreach ( $this->wc_taxes() as $tax ) {
928 $this->response['selectedTaxRates'][] = $tax['id'] . '^^' . get_option( 'cmbird_zoho_inventory_tax_rate_' . $tax['id'] );
929 }
930 $this->serve();
931 }
932
933 /**
934 * Retrieves an array of all tax rates from WooCommerce.
935 *
936 * @return array An array of all tax rates.
937 */
938 public function wc_taxes(): array {
939 $wc_tax_array = array();
940 $tax_classes = WC_Tax::get_tax_classes(); // Retrieve all tax classes.
941 if ( ! in_array( '', $tax_classes ) ) { // Make sure "Standard rate" (empty class name) is present.
942 array_unshift( $tax_classes, '' );
943 }
944
945 foreach ( $tax_classes as $tax_class ) { // For each tax class, get all rates.
946 $taxes = WC_Tax::get_rates_for_tax_class( $tax_class );
947
948 foreach ( $taxes as $key => $tax ) {
949 $taxarray = (array) $tax;
950 $taxarray['id'] = $key;
951 $wc_tax_array[] = $taxarray;
952 }
953 }
954
955 return $wc_tax_array;
956 }
957
958 /**
959 * Resets the tax settings.
960 *
961 * This function verifies the tax settings and then deletes the options
962 * related to the tax rates and tax groups.
963 *
964 * @return void
965 */
966 public function tax_reset(): void {
967 $this->verify();
968 foreach ( $this->wc_taxes() as $tax ) {
969 delete_option( 'cmbird_zoho_inventory_tax_rate_' . $tax['id'] );
970 }
971 delete_option( 'cmbird_zi_tax_groups' );
972 $this->response = array( 'message' => 'Reset successfully!' );
973 $this->serve();
974 }
975
976 /**
977 * Saves tax rates to the database.
978 */
979 public function tax_set(): void {
980 $this->verify( self::FORMS['tax'] );
981 $rates = array_filter( $this->data['selectedTaxRates'] );
982 try {
983 foreach ( $rates as $value ) {
984 $valarray = explode( '^^', $value );
985 update_option( 'cmbird_zoho_inventory_tax_rate_' . $valarray[0], $valarray[1] );
986 }
987 $this->response = array( 'message' => 'Saved' );
988 } catch ( Throwable $throwable ) {
989 $this->errors = array( 'message' => $throwable->getMessage() );
990 }
991
992 $this->serve();
993 }
994
995 /**
996 * Sets the connection for the Zoho Inventory API.
997 */
998 public function connection_set(): void {
999 $this->verify(
1000 array(
1001 'account_domain',
1002 'organization_id',
1003 'client_id',
1004 'client_secret',
1005 'redirect_uri',
1006 ),
1007 );
1008 try {
1009 $domain = CMBIRD_Auth_Zoho::normalize_domain( $this->data['account_domain'] );
1010 $inventory = sprintf( 'https://www.zohoapis.%s/', $domain );
1011 $oauth_args = array(
1012 'response_type' => 'code',
1013 'client_id' => $this->data['client_id'],
1014 'scope' => 'ZohoInventory.FullAccess.all',
1015 'redirect_uri' => $this->data['redirect_uri'],
1016 'prompt' => 'consent',
1017 'access_type' => 'offline',
1018 'state' => wp_create_nonce( Template::NAME ),
1019 );
1020
1021 update_option( 'cmbird_zoho_inventory_domain', $domain );
1022 update_option( 'cmbird_zoho_inventory_oid', $this->data['organization_id'] );
1023 update_option( 'cmbird_zoho_inventory_cid', $this->data['client_id'] );
1024 update_option( 'cmbird_zoho_inventory_cs', $this->data['client_secret'] );
1025 update_option( 'cmbird_zoho_inventory_url', $inventory );
1026 update_option( 'cmbird_authorization_redirect_uri', $this->data['redirect_uri'] );
1027 $redirect_base = CMBIRD_Auth_Zoho::get_auth_redirect_url_for_domain( $domain );
1028 $redirect = esc_url_raw( add_query_arg( $oauth_args, $redirect_base ) );
1029 $this->response = array(
1030 'redirect' => $redirect,
1031 'message' => 'We are redirecting you to zoho. please wait...',
1032 );
1033 } catch ( Throwable $throwable ) {
1034 $this->errors = array( 'message' => $throwable->getMessage() );
1035 }
1036
1037 $this->serve();
1038 }
1039
1040 /**
1041 * Resets the connection by deleting specific options from the database.
1042 */
1043 public function connection_reset(): void {
1044 $this->verify();
1045 try {
1046 // Check if resetAll is requested (sent from Vue).
1047 $reset_all = isset( $this->data['reset'] ) && ( $this->data['reset'] === 1 || $this->data['reset'] === true || $this->data['reset'] === 'true' );
1048
1049 // Safety: require an explicit confirmation token for destructive, global reset.
1050 $confirm_token = isset( $this->data['confirm'] ) ? $this->data['confirm'] : '';
1051
1052 if ( $reset_all ) {
1053 // The client must pass confirm: 'reset-all' to perform the full destructive reset.
1054 if ( 'reset-all' !== $confirm_token ) {
1055 $this->errors = array( 'message' => 'Missing reset confirmation.' );
1056 $this->serve();
1057 return;
1058 }
1059
1060 // Audit log for full reset (important to have server-side evidence).
1061 $this->write_log(
1062 array(
1063 'action' => 'full_reset_triggered',
1064 'user_id' => get_current_user_id(),
1065 ),
1066 'zoho-reset'
1067 );
1068
1069 global $wpdb;
1070 // Delete all product and customer meta keys.
1071 $wpdb->query( "DELETE FROM {$wpdb->prefix}postmeta WHERE meta_key IN ('zi_item_id', 'zi_category_id')" );
1072 $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')" );
1073
1074 // Delete all plugin options that belong to Zoho namespace.
1075 $wpdb->query(
1076 $wpdb->prepare(
1077 "DELETE FROM {$wpdb->prefix}options WHERE option_name LIKE %s",
1078 'cmbird_zoho_%'
1079 )
1080 );
1081
1082 $this->response['message'] = __( 'All settings are reset successfully', 'commercebird' );
1083 $this->serve();
1084 return;
1085 }
1086
1087 // Non-destructive reset (connection settings only).
1088 $options = array(
1089 'cmbird_zoho_inventory_domain',
1090 'cmbird_zoho_inventory_oid',
1091 'cmbird_zoho_inventory_cid',
1092 'cmbird_zoho_inventory_cs',
1093 'cmbird_zoho_inventory_url',
1094 'cmbird_zoho_inventory_access_token',
1095 );
1096 foreach ( $options as $zi_option ) {
1097 delete_option( $zi_option );
1098 }
1099 $this->response = array( 'message' => 'Reset successfully!' );
1100
1101 } catch ( Throwable $throwable ) {
1102 $this->errors = array( 'message' => $throwable->getMessage() );
1103 }
1104
1105 $this->serve();
1106 }
1107
1108 /**
1109 * Retrieves the Zoho tax rates for the organization.
1110 *
1111 * This function verifies the request, retrieves the organization ID and URL
1112 * from the options, constructs the URL for the API request, and executes
1113 * the cURL call to fetch the tax rates. If the response contains the
1114 * 'taxes' key, it sets the response to the tax rates and saves tax groups
1115 * separately. Otherwise, it sets the errors to a default message.
1116 */
1117 public function zoho_tax_rates_collect(): void {
1118 $this->verify();
1119 $zoho_inventory_oid = get_option( 'cmbird_zoho_inventory_oid' );
1120 $zoho_inventory_url = get_option( 'cmbird_zoho_inventory_url' );
1121 $url = $zoho_inventory_url . 'inventory/v1/settings/taxes?organization_id=' . $zoho_inventory_oid;
1122 try {
1123 $execute_curl_call_handle = new CMBIRD_API_Handler_Zoho();
1124 $json = (array) $execute_curl_call_handle->execute_curl_call_get( $url );
1125 if ( array_key_exists( 'taxes', $json ) ) {
1126 $this->response = $json['taxes'];
1127
1128 // Save tax groups separately for compound tax handling.
1129 $tax_groups = array();
1130 foreach ( $json['taxes'] as $tax ) {
1131 if ( isset( $tax->tax_type ) && 'tax_group' === $tax->tax_type ) {
1132 $tax_groups[] = array(
1133 'tax_id' => $tax->tax_id,
1134 'tax_percentage' => $tax->tax_percentage,
1135 'tax_name' => $tax->tax_name,
1136 );
1137 }
1138 }
1139 update_option( 'cmbird_zi_tax_groups', $tax_groups, false );
1140 } else {
1141 $this->errors = array( 'message' => 'Something went wrong!' );
1142 }
1143 } catch ( Throwable $throwable ) {
1144 $this->errors = array( 'message' => $throwable->getMessage() );
1145 }
1146
1147 $this->serve();
1148 }
1149
1150 /**
1151 * Executes the wc_tax_collect function.
1152 *
1153 * This function verifies the current state, retrieves the taxes using the
1154 * wc_taxes method, and serves the response.
1155 */
1156 public function wc_tax_collect(): void {
1157 $this->verify();
1158 $this->response = $this->wc_taxes();
1159 $this->serve();
1160 }
1161
1162 /**
1163 * Handles the oAuth code.
1164 */
1165 public function handle_code(): void {
1166 $this->verify();
1167 if ( array_key_exists( 'code', $this->request ) ) {
1168 $class_functions = new CMBIRD_Auth_Zoho();
1169 $code = $this->request['code'];
1170 $state = $this->request['state'] ?? '';
1171
1172 if ( empty( $state ) || ! wp_verify_nonce( $state, Template::NAME ) ) {
1173 $this->errors = array( 'message' => 'Invalid OAuth state. Please try connecting again.' );
1174 $this->serve();
1175 return;
1176 }
1177
1178 try {
1179 $access_token = $class_functions->get_zoho_access_token( $code, 'zoho_inventory' );
1180 if ( is_wp_error( $access_token ) ) {
1181 $this->errors = array( 'message' => $access_token->get_error_message() );
1182 } elseif ( is_array( $access_token ) && array_key_exists( 'access_token', $access_token ) ) {
1183 update_option( 'cmbird_zoho_inventory_auth_code', $code );
1184 update_option( 'cmbird_zoho_inventory_access_token', $access_token['access_token'] );
1185 update_option( 'cmbird_zoho_inventory_refresh_token', $access_token['refresh_token'] );
1186 update_option( 'cmbird_zoho_inventory_timestamp', strtotime( gmdate( 'Y-m-d H:i:s' ) ) + $access_token['expires_in'] );
1187 $this->response = (array) $access_token;
1188 } else {
1189 $this->errors = array( 'message' => 'Unable to retrieve Zoho Inventory access token.' );
1190 }
1191 } catch ( Throwable $throwable ) {
1192 $this->errors = array( 'message' => $throwable->getMessage() );
1193 }
1194 }
1195 $this->serve();
1196 }
1197
1198 /**
1199 * Converts all guest users to a registered users.
1200 *
1201 * This function verifies the request, retrieves all guest orders
1202 *
1203 * @return void
1204 */
1205 public function convert_guest(): void {
1206 $this->verify();
1207 $args = array(
1208 'status' => array( 'wc-processing', 'wc-completed' ),
1209 'return' => 'ids',
1210 'customer' => 0, // Only guest orders.
1211 'limit' => -1, // Retrieve all guest orders.
1212 );
1213
1214 $orders = wc_get_orders( $args );
1215 if ( empty( $orders ) ) {
1216 $this->response = array( 'message' => __( 'No guest orders found.', 'commercebird' ) );
1217 $this->serve();
1218 return;
1219 }
1220 $count = count( $orders );
1221 foreach ( $orders as $order_id ) {
1222 $order = wc_get_order( $order_id );
1223 $email = $order->get_billing_email();
1224
1225 if ( email_exists( $email ) === false ) {
1226 $username = strstr( $email, '@', true );
1227 $password = wp_generate_password();
1228 $user_id = wc_create_new_customer( $email, $username, $password );
1229
1230 if ( ! is_wp_error( $user_id ) ) {
1231 // use update_meta to set the user ID for the order.
1232 $order->set_customer_id( $user_id );
1233 $order->save();
1234 }
1235 } else {
1236 // If the email already exists, we can still update the order to the existing user.
1237 $user = get_user_by( 'email', $email );
1238 if ( $user ) {
1239 $order->set_customer_id( $user->ID );
1240 $order->save();
1241 }
1242 }
1243 }
1244 /* translators: %d: Number of orders converted */
1245 $this->response['message'] = sprintf( __( 'Successfully converted %d guest orders to registered users.', 'commercebird' ), $count );
1246 $this->serve();
1247 } /**
1248 * Fetches webhooks from Zoho Inventory API.
1249 *
1250 * @return void
1251 */
1252 public function get_zoho_webhooks(): void {
1253 $this->verify();
1254 $zoho_inventory_oid = get_option( 'cmbird_zoho_inventory_oid' );
1255 $zoho_inventory_url = get_option( 'cmbird_zoho_inventory_url' );
1256 if ( ! $zoho_inventory_oid || ! $zoho_inventory_url ) {
1257 $this->errors = array( 'message' => 'Missing Zoho organization ID or domain.' );
1258 $this->serve();
1259 return;
1260 }
1261 $url = $zoho_inventory_url . 'inventory/v1/settings/webhooks?page=1&per_page=50&usestate=false&organization_id=' . $zoho_inventory_oid;
1262 try {
1263 $execute_curl_call_handle = new CMBIRD_API_Handler_Zoho();
1264 $json = $execute_curl_call_handle->execute_curl_call_get( $url );
1265
1266 // Check if the response is a WP_Error object.
1267 if ( is_wp_error( $json ) ) {
1268 $this->errors = array( 'message' => $json->get_error_message() );
1269 } elseif ( isset( $json->webhooks ) && is_array( $json->webhooks ) ) {
1270 $this->response = $json->webhooks;
1271 } else {
1272 $this->errors = array( 'message' => 'Failed to fetch webhooks or no webhooks found.' );
1273 }
1274 } catch ( Throwable $throwable ) {
1275 $this->errors = array( 'message' => $throwable->getMessage() );
1276 }
1277 $this->serve();
1278 }
1279
1280 /**
1281 * Function to sync variable items from Zoho to WooCommerce.
1282 */
1283 public function sync_variable_items_from_zoho(): void {
1284 $this->verify();
1285
1286 // Clear Orphan data.
1287 $zi_common_class = new \CMBIRD_Common_Functions();
1288 $zi_common_class->clear_orphan_data();
1289
1290 // check if a category is selected.
1291 $selected_category = isset( $_GET['category'] ) ? sanitize_text_field( $_GET['category'] ) : null;
1292
1293 if ( $selected_category ) {
1294 $categories = array( $selected_category ); // Only sync the selected category.
1295 } else {
1296 $zoho_item_category = get_option( 'cmbird_zoho_item_category' );
1297 if ( $zoho_item_category ) {
1298 // convert serialized string to array.
1299 $categories = maybe_unserialize( $zoho_item_category );
1300 if ( ! is_array( $categories ) ) {
1301 $categories = array();
1302 }
1303 } else {
1304 $categories = array();
1305 }
1306 }
1307
1308 if ( empty( $categories ) ) {
1309 $this->errors = array( 'message' => __( 'Please select at least one category from cron tab', 'commercebird' ) );
1310 } else {
1311 foreach ( $categories as $category_index => $category_id ) {
1312 $data = array(
1313 'page' => 1,
1314 'category' => $category_id,
1315 );
1316 $existing_schedule = as_has_scheduled_action( 'import_group_items_cron', $data );
1317 // Schedule the action if it doesn't exist.
1318 if ( ! $existing_schedule ) {
1319 as_schedule_single_action( time(), 'import_group_items_cron', $data );
1320 }
1321 }
1322 $this->response = array( 'message' => 'Items are being imported in background. You can visit other tabs :).' );
1323 }
1324 $this->serve();
1325 }
1326
1327 /**
1328 * Function to sync simple items from Zoho to WooCommerce.
1329 */
1330 public function sync_simple_items_from_zoho(): void {
1331 $this->verify();
1332
1333 // Clear Orphan data.
1334 $zi_common_class = new \CMBIRD_Common_Functions();
1335 $zi_common_class->clear_orphan_data();
1336
1337 // Check if a category is selected.
1338 // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Admin sync action, nonce verified in parent.
1339 $selected_category = isset( $_GET['category'] ) ? sanitize_text_field( wp_unslash( $_GET['category'] ) ) : null;
1340
1341 if ( $selected_category ) {
1342 $categories = array( $selected_category ); // Only sync the selected category.
1343 } else {
1344 $zoho_item_category = get_option( 'cmbird_zoho_item_category' );
1345 if ( $zoho_item_category ) {
1346 // convert serialized string to array.
1347 $categories = maybe_unserialize( $zoho_item_category );
1348 if ( ! is_array( $categories ) ) {
1349 $categories = array();
1350 }
1351 } else {
1352 $categories = array();
1353 }
1354 }
1355
1356 if ( empty( $categories ) ) {
1357 $this->errors = array( 'message' => __( 'Please select at least one category from cron tab', 'commercebird' ) );
1358 } else {
1359 foreach ( $categories as $index => $category_id ) {
1360 $data = array(
1361 'page' => 1,
1362 'category' => $category_id,
1363 );
1364 // Add group parameter to prevent scheduling conflicts.
1365 $existing_schedule = as_has_scheduled_action( 'import_simple_items_cron', $data, 'commercebird' );
1366 if ( ! $existing_schedule ) {
1367 as_schedule_single_action( time(), 'import_simple_items_cron', $data, 'commercebird' );
1368 }
1369 }
1370 $this->response = array( 'message' => __( 'Items are being imported in background. You can visit other tabs :).', 'commercebird' ) );
1371 }
1372 $this->serve();
1373 }
1374
1375
1376 /**
1377 * Schedule a batch of WooCommerce product IDs for Zoho sync.
1378 *
1379 * Wrapped in a protected method to make testing easier.
1380 *
1381 * @param int[] $ids Array of post IDs to sync.
1382 * @return void
1383 */
1384 protected function schedule_zi_product( array $ids ): void {
1385 as_schedule_single_action( time(), 'sync_zi_product_cron', array( $ids ) );
1386 }
1387
1388 /**
1389 * Resolve the stored Zoho category IDs into WooCommerce product_cat term IDs.
1390 *
1391 * The option 'cmbird_zoho_item_category' is a plain serialised array of Zoho
1392 * category IDs. For each ID the corresponding WC term ID is resolved by
1393 * querying the per-term options stored as 'cmbird_zoho_id_for_term_id_{term_id}'.
1394 *
1395 * @param mixed $opt The raw option value returned by get_option().
1396 * @return int[] List of WC product_cat term IDs (empty array on failure).
1397 */
1398 private function normalize_category_list( $opt ): array {
1399 if ( empty( $opt ) ) {
1400 return array();
1401 }
1402
1403 $zoho_cat_ids = maybe_unserialize( $opt );
1404 if ( ! is_array( $zoho_cat_ids ) ) {
1405 return array();
1406 }
1407
1408 $map = new \CommerceBird\Admin\Mappings\ZohoCategoryMap();
1409 $term_ids = array();
1410 foreach ( $zoho_cat_ids as $zoho_id ) {
1411 if ( empty( $zoho_id ) ) {
1412 continue;
1413 }
1414 $term_id = $map->term_id_for_zoho_id_any_status( (string) $zoho_id );
1415 if ( null !== $term_id ) {
1416 $term_ids[] = $term_id;
1417 }
1418 }
1419
1420 return array_values( array_unique( $term_ids ) );
1421 }
1422
1423 /**
1424 * Return all published products in the given categories which do not have a
1425 * Zoho item ID yet.
1426 *
1427 * @param int[] $categories Array of product_cat term IDs to filter by.
1428 * @return int[] List of post IDs.
1429 */
1430 protected function get_unsynced_product_ids( array $categories ): array {
1431 // make sure we actually have some categories to query against.
1432 if ( empty( $categories ) ) {
1433 return array();
1434 }
1435
1436 // cast everything to int just in case the caller passed strings.
1437 $categories = array_map( 'intval', $categories );
1438
1439 $args = array(
1440 'post_type' => 'product',
1441 'post_status' => 'publish',
1442 'fields' => 'ids',
1443 'posts_per_page' => -1,
1444 'tax_query' => array(
1445 array(
1446 'taxonomy' => 'product_cat',
1447 'field' => 'term_id',
1448 'terms' => $categories,
1449 ),
1450 ),
1451 // use a meta_query so the database only returns posts without the meta key.
1452 'meta_query' => array(
1453 array(
1454 'key' => 'zi_item_id',
1455 'compare' => 'NOT EXISTS',
1456 ),
1457 ),
1458 );
1459
1460 // WP_Query handles the heavy lifting; since we asked for IDs only we get a simple array.
1461 $query = new \WP_Query( $args );
1462
1463 return $query->posts ?: array();
1464 }
1465
1466 /**
1467 * Function to sync items from WooCommerce to Zoho.
1468 */
1469 public function sync_items_to_zoho(): void {
1470 $this->verify();
1471
1472 // figure out which categories should be considered for this sync; it can.
1473 // be passed explicitly via GET or fall back to the stored cron setting.
1474 $selected_category = isset( $_GET['category'] ) ? sanitize_text_field( wp_unslash( $_GET['category'] ) ) : null;
1475
1476 if ( $selected_category ) {
1477 $categories = array( $selected_category );
1478 } else {
1479 $categories = $this->normalize_category_list( get_option( 'cmbird_zoho_item_category' ) );
1480 }
1481
1482 if ( empty( $categories ) ) {
1483 $this->errors = array( 'message' => __( 'Please select at least one category from category page', 'commercebird' ) );
1484 $this->serve();
1485 return;
1486 }
1487
1488 // get all unsynced products within those categories.
1489 $post_ids = $this->get_unsynced_product_ids( $categories );
1490
1491 if ( empty( $post_ids ) ) {
1492 $this->response = array( 'message' => 'No items to sync.' );
1493 $this->serve();
1494 return;
1495 } // get all unsynced products within those categories.
1496 $post_ids = $this->get_unsynced_product_ids( $categories );
1497
1498 if ( empty( $post_ids ) ) {
1499 $this->response = array( 'message' => 'No items to sync.' );
1500 $this->serve();
1501 return;
1502 }
1503
1504 $product_ids = array();
1505 foreach ( $post_ids as $post_id ) {
1506 $product_ids[] = $post_id;
1507 if ( count( $product_ids ) === 10 ) {
1508 $this->schedule_zi_product( $product_ids );
1509 $product_ids = array();
1510 }
1511 }
1512
1513 if ( count( $product_ids ) > 0 ) {
1514 $this->schedule_zi_product( $product_ids );
1515 }
1516
1517 $this->response = array( 'message' => 'Products are being synced in background.' );
1518 $this->serve();
1519 }
1520
1521 /**
1522 * Sync contacts from Zoho to WooCommerce.
1523 */
1524 public function sync_contacts_from_zoho(): void {
1525 $this->verify();
1526
1527 $page = 1;
1528 $data_arr = (object) array();
1529 $data_arr->page = $page;
1530 $existing_schedule = as_has_scheduled_action( 'sync_zi_import_contacts', array( $data_arr ) );
1531
1532 // Wrap this via Action Scheduler per page.
1533 if ( ! $existing_schedule ) {
1534 // Schedule the cron job.
1535 as_schedule_single_action( time(), 'sync_zi_import_contacts', array( $data_arr ) );
1536 }
1537
1538 $this->response = array( 'message' => 'Syncing in background. You can visit other tabs :).' );
1539 $this->serve();
1540 }
1541
1542 /**
1543 * Sync composite items from Zoho to WooCommerce.
1544 */
1545 public function sync_composite_items_from_zoho(): void {
1546 $this->verify();
1547
1548 // Clear Orphan data.
1549 $zi_common_class = new \CMBIRD_Common_Functions();
1550 $zi_common_class->clear_orphan_data();
1551
1552 // check if a category is selected.
1553 $selected_category = isset( $_GET['category'] ) ? sanitize_text_field( $_GET['category'] ) : null;
1554 if ( $selected_category ) {
1555 $categories = array( $selected_category ); // Only sync the selected category.
1556 } else {
1557 $zoho_item_category = get_option( 'cmbird_zoho_item_category' );
1558 if ( $zoho_item_category ) {
1559 // convert serialized string to array.
1560 $categories = maybe_unserialize( $zoho_item_category );
1561 if ( ! is_array( $categories ) ) {
1562 $categories = array();
1563 }
1564 } else {
1565 $categories = array();
1566 }
1567 }
1568
1569 $item_add_resp = array();
1570 foreach ( $categories as $category_id ) {
1571 $product_class = new \CMBIRD_Products_ZI();
1572 $response = $product_class->recursively_sync_composite_item_from_zoho( 1, $category_id );
1573 $item_add_resp = array_merge( $item_add_resp, $response );
1574 }
1575
1576 // Send log message to admin if there are responses.
1577 if ( ! empty( $item_add_resp ) ) {
1578 $this->send_log_message_to_admin( $item_add_resp, 'Log Message for manual sync', 'Composite item sync from zoho' );
1579 }
1580
1581 $this->response = $item_add_resp;
1582 $this->serve();
1583 }
1584
1585 /**
1586 * Sync composite items from WooCommerce to Zoho.
1587 */
1588 public function sync_composite_items_to_zoho(): void {
1589 $this->verify();
1590
1591 $irgs = array(
1592 'post_type' => array( 'product' ),
1593 'posts_per_page' => '-1',
1594 'post_status' => 'publish',
1595 'tax_query' => array(
1596 array(
1597 'taxonomy' => 'product_type',
1598 'field' => 'slug',
1599 'terms' => 'bundle',
1600 ),
1601 ),
1602 );
1603
1604 $my_query = new \WP_Query( $irgs );
1605 $bundled_product = $my_query->posts;
1606 if ( count( $bundled_product ) > 0 ) {
1607 foreach ( $bundled_product as $prod ) {
1608 $bundle_childs = \WC_PB_DB::query_bundled_items(
1609 array(
1610 'return' => 'id=>product_id', // 'objects'.
1611 'bundle_id' => array( $prod->ID ),
1612 )
1613 );
1614 if ( count( $bundle_childs ) > 0 ) {
1615 foreach ( $bundle_childs as $child_item_id ) {
1616 $zoho_item_id = get_post_meta( $child_item_id, 'zi_item_id', true );
1617 if ( empty( $zoho_item_id ) ) {
1618 break;
1619 }
1620 }
1621 }
1622 }
1623 }
1624
1625 $this->response = array(
1626 'message' => 'Sync Started',
1627 'code' => 200,
1628 );
1629 $this->serve();
1630 }
1631
1632 /**
1633 * Send log message to admin.
1634 *
1635 * @param array $sync_logs Log messages.
1636 * @param string $subject Email subject.
1637 * @param string $message Email message.
1638 */
1639 private function send_log_message_to_admin( $sync_logs, $subject, $message ): void {
1640 $table_root = "<h3>$message</h3>";
1641 $table_root .= '<table><thead><tr><th>Action</th><th> Log message</th></tr></thead><tbody>';
1642
1643 foreach ( $sync_logs as $logs ) {
1644 $table_root .= "<tr><td>$logs->resp_id</td><td>$logs->message</td></tr>";
1645 }
1646 $table_root .= '</tbody></table>';
1647 $common_class = new \CMBIRD_Common_Functions();
1648 $common_class->send_email( $subject, $table_root );
1649 }
1650 }
1651