PluginProbe ʕ •ᴥ•ʔ
CommerceBird – AI Command Center, ERP Integrations & B2B for WooCommerce (Zoho, Exact Online). / 2.7.0
CommerceBird – AI Command Center, ERP Integrations & B2B for WooCommerce (Zoho, Exact Online). v2.7.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 / ZohoCRMAjax.php
commercebird / admin / includes / Actions / Ajax Last commit date
AcfAjax.php 7 months ago ExactOnlineAjax.php 5 months ago SettingsAjax.php 7 months ago ZohoCRMAjax.php 7 months ago ZohoInventoryAjax.php 5 months ago index.php 1 year ago
ZohoCRMAjax.php
972 lines
1 <?php
2
3 namespace CommerceBird\Admin\Actions\Ajax;
4
5 use CMBIRD_Auth_Zoho;
6 use CMBIRD_API_Handler_Zoho;
7 use CommerceBird\Admin\Actions\Sync\ZohoCRMSync;
8 use CommerceBird\Admin\Template;
9 use CommerceBird\Admin\Traits\AjaxRequest;
10 use CommerceBird\Admin\Traits\OptionStatus;
11 use CommerceBird\Admin\Traits\Singleton;
12 use CommerceBird\Admin\Traits\LogWriter;
13 use WC_Tax;
14 use Throwable;
15
16
17 if ( ! defined( 'ABSPATH' ) ) {
18 exit;
19 }
20 /**
21 * Initializes the Zoho CRM class.
22 *
23 * @since 1.0.0
24 * @return void
25 */
26 final class ZohoCRMAjax {
27
28 use Singleton;
29 use AjaxRequest;
30 use OptionStatus;
31 use LogWriter;
32
33
34
35 private const FORMS = array(
36 // 'connect' => array(.
37 // 'token',.
38 // ),.
39 'product' => array(
40 'importProducts',
41 ),
42 'order' => array( 'range' ),
43 'order_settings' => array(
44 'syncAsDeals',
45 ),
46 'order_field_mapping' => array(
47 'dealFields',
48 ),
49 'order_status_stage_mapping' => array(
50 'mapping',
51 ),
52 'customer' => array(
53 'importCustomers',
54 ),
55 'contact_settings' => array(
56 'syncCustomers',
57 'syncOnRegistration',
58 'syncOnUpdate',
59 ),
60 'contact_layout' => array(
61 'layoutId',
62 ),
63 'fields' => array(
64 'form',
65 'module',
66 ),
67 'tax' => array(
68 'selectedTaxRates',
69 ),
70 );
71 private const ACTIONS = array(
72 'get_zcrm_connect' => 'connection_get',
73 'save_zcrm_connect' => 'connection_set',
74 'reset_zcrm_connect' => 'connection_reset',
75 'is_zcrm_connected' => 'connection_done',
76 'import_zcrm_product' => 'product_import',
77 'map_zcrm_product' => 'product_map',
78 'map_zcrm_customer' => 'customer_map',
79 'map_zcrm_order' => 'order_map',
80 'export_zcrm_order' => 'order_export',
81 'get_zcrm_order_settings' => 'order_settings_get',
82 'save_zcrm_order_settings' => 'order_settings_save',
83 'get_zcrm_deal_fields' => 'deal_fields_get',
84 'get_zcrm_deal_field_mapping' => 'deal_field_mapping_get',
85 'save_zcrm_deal_field_mapping' => 'deal_field_mapping_save',
86 'get_wc_order_statuses' => 'wc_order_statuses_get',
87 'get_zcrm_deal_stages' => 'deal_stages_get',
88 'get_order_status_stage_mapping' => 'order_status_stage_mapping_get',
89 'save_order_status_stage_mapping' => 'order_status_stage_mapping_save',
90 'refresh_zcrm_fields' => 'refresh_zcrm_fields',
91 'zcrm_get_custom_fields' => 'zcrm_get_custom_fields',
92 'zcrm_save_custom_fields' => 'zcrm_save_custom_fields',
93 'zcrm_reset_custom_fields' => 'zcrm_reset_custom_fields',
94 'zcrm_fields' => 'get_zcrm_fields',
95 'zcrm_handle_code' => 'handle_code',
96 'get_zcrm_tax' => 'tax_get',
97 'save_zcrm_tax' => 'tax_set',
98 'reset_zcrm_tax' => 'tax_reset',
99 'get_wc_taxes' => 'wc_tax_collect',
100 'get_zcrm_taxes' => 'zoho_tax_rates_collect',
101 'get_zcrm_layouts' => 'layouts_get',
102 'save_zcrm_layout' => 'layout_save',
103 'get_zcrm_selected_layout' => 'layout_get_selected',
104 'export_zcrm_contact' => 'contact_export',
105 'get_zcrm_contact_layouts' => 'contact_layouts_get',
106 'save_zcrm_contact_layout' => 'contact_layout_save',
107 'get_zcrm_selected_contact_layout' => 'contact_layout_get_selected',
108 );
109 private const OPTIONS = array(
110 // 'connect' => array(.
111 // 'token' => 'commercebird-exact-online-token',.
112 // ),.
113 'zcrm_sales_orders_fields' => 'zcrm_sales_orders_fields',
114 'zcrm_contacts_fields' => 'zcrm_contacts_fields',
115 'zcrm_products_fields' => 'zcrm_products_fields',
116 'zcrm_sales_order_layout' => 'zcrm_sales_order_layout',
117 'zcrm_contact_layout' => 'zcrm_contact_layout',
118 );
119
120 public function __construct() {
121 $this->load_actions();
122 }
123
124 /**
125 * Sets the connection for the Zoho crm API.
126 */
127 public function connection_set(): void {
128 $this->verify(
129 array(
130 'account_domain',
131 'client_id',
132 'client_secret',
133 'redirect_uri',
134 ),
135 );
136 try {
137 $crm_url = sprintf( 'https://www.zohoapis.%s/', $this->data['account_domain'] );
138 // Special handling for Canada domain.
139 if ( 'ca' === $this->data['account_domain'] ) {
140 $crm_url = 'https://www.zohoapis.ca/';
141 }
142 update_option( 'cmbird_zoho_crm_domain', $this->data['account_domain'] );
143 update_option( 'cmbird_zoho_crm_cid', $this->data['client_id'] );
144 update_option( 'cmbird_zoho_crm_cs', $this->data['client_secret'] );
145 update_option( 'cmbird_zoho_crm_url', $crm_url );
146 update_option( 'cmbird_authorization_redirect_uri', $this->data['redirect_uri'] );
147 $account_url = sprintf( 'https://accounts.zoho.%s', $this->data['account_domain'] );
148 // if account domain is ca then set the account_url to https://accounts.zohocloud.ca/.
149 if ( 'ca' === $this->data['account_domain'] ) {
150 $account_url = 'https://accounts.zohocloud.ca';
151 }
152 $redirect = esc_url_raw(
153 $account_url
154 . '/oauth/v2/auth?response_type=code&client_id='
155 . $this->data['client_id'] . '&scope=ZohoCRM.users.ALL,ZohoCRM.bulk.ALL,ZohoCRM.modules.ALL,ZohoCRM.settings.ALL,ZohoCRM.org.ALL,profile.userphoto.READ,ZohoFiles.files.CREATE&redirect_uri='
156 . $this->data['redirect_uri'] . '&prompt=consent&access_type=offline&state='
157 . wp_create_nonce( Template::NAME )
158 );
159 $this->response = array(
160 'redirect' => $redirect,
161 'message' => 'We are redirecting you to zoho. please wait...',
162 );
163 } catch ( Throwable $throwable ) {
164 $this->errors = array( 'message' => $throwable->getMessage() );
165 }
166
167 $this->serve();
168 }
169
170 /**
171 * Retrieves the connection details.
172 *
173 * @return void
174 */
175 public function connection_get(): void {
176 $this->verify();
177 $this->response['account_domain'] = get_option( 'cmbird_zoho_crm_domain' );
178 $this->response['client_id'] = get_option( 'cmbird_zoho_crm_cid' );
179 $this->response['client_secret'] = get_option( 'cmbird_zoho_crm_cs' );
180 $this->response['crm_url'] = get_option( 'cmbird_zoho_crm_url' );
181 $this->response['redirect_uri'] = get_option( 'cmbird_authorization_redirect_uri' );
182 $this->serve();
183 }
184
185 /**
186 * Handles the oAuth code.
187 */
188 public function handle_code(): void {
189 $this->verify();
190 if ( array_key_exists( 'code', $this->request ) ) {
191 $class_functions = new CMBIRD_Auth_Zoho();
192 $code = $this->request['code'];
193 try {
194 $access_token = $class_functions->get_zoho_access_token( $code, 'zoho_crm' );
195 if ( is_array( $access_token ) && array_key_exists( 'access_token', $access_token ) ) {
196 update_option( 'cmbird_zoho_crm_auth_code', $code );
197 update_option( 'cmbird_zoho_crm_access_token', $access_token['access_token'] );
198 update_option( 'cmbird_zoho_crm_refresh_token', $access_token['refresh_token'] );
199 update_option( 'cmbird_zoho_crm_timestamp', strtotime( gmdate( 'Y-m-d H:i:s' ) ) + $access_token['expires_in'] );
200 $this->response = (array) $access_token;
201 } else {
202 $this->errors = (array) $access_token;
203 }
204 } catch ( Throwable $throwable ) {
205 $this->errors = array( 'message' => $throwable->getMessage() );
206 }
207 }
208 $this->serve();
209 }
210
211 /**
212 * Resets the connection by deleting specific options from the database.
213 */
214 public function connection_reset(): void {
215 $this->verify();
216 try {
217 $options = array(
218 'cmbird_zoho_crm_domain',
219 'cmbird_zoho_crm_cid',
220 'cmbird_zoho_crm_cs',
221 'cmbird_zoho_crm_url',
222 'cmbird_zoho_crm_access_token',
223 );
224 foreach ( $options as $zi_option ) {
225 delete_option( $zi_option );
226 }
227 $this->response = array( 'message' => 'Reset successfully!' );
228 } catch ( Throwable $throwable ) {
229 $this->errors = array( 'message' => $throwable->getMessage() );
230 }
231
232 $this->serve();
233 }
234
235
236 public function connection_done() {
237 $this->verify();
238 $zoho_crm_url = get_option( 'cmbird_zoho_crm_url' );
239 $url = $zoho_crm_url . 'crm/v7/org';
240 $execute_curl_call_handle = new CMBIRD_API_Handler_Zoho();
241 $json = $execute_curl_call_handle->execute_curl_call_get( $url );
242 if ( is_wp_error( $json ) ) {
243 $this->errors = array( 'message' => $json->get_error_message() );
244 } elseif ( empty( $json ) ) {
245 $this->errors = array( 'message' => 'We lost connection with zoho. please refresh page.' );
246 } else {
247 $this->response = ! empty( $json->org ) ? (array) $json->org[0] : array();
248 // schedule a wp cron to refresh the token after one hour from now.
249 if ( ! wp_next_scheduled( 'zcrm_refresh_token' ) ) {
250 wp_schedule_event( time() + 3600, 'hourly', 'zcrm_refresh_token' );
251 } else {
252 // unschedule first and reschedule.
253 wp_unschedule_event( wp_next_scheduled( 'zcrm_refresh_token' ), 'zcrm_refresh_token' );
254 wp_schedule_event( time() + 3600, 'hourly', 'zcrm_refresh_token' );
255 }
256 }
257 $this->serve();
258 }
259
260 /**
261 * Get Zoho CRM custom fields.
262 *
263 * @since 1.0.0
264 * @return void
265 */
266 public function refresh_zcrm_fields() {
267 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Admin-only AJAX call, nonce verified in parent.
268 $module = isset( $_GET['module'] ) ? sanitize_text_field( wp_unslash( $_GET['module'] ) ) : '';
269 if ( empty( $module ) ) {
270 $this->errors['message'] = 'Module name is required.';
271 } else {
272 $fields = ( new ZohoCRMSync() )->get_custom_fields( $module );
273 if ( is_wp_error( $fields ) ) {
274 $this->errors['message'] = $fields->get_error_message();
275 } else {
276 $option_name = 'zcrm_' . strtolower( $module ) . '_fields';
277 update_option( self::OPTIONS[ $option_name ], $fields );
278 $this->response = array( 'message' => 'Refresh successfully!' );
279 $this->response['fields'] = $fields;
280 }
281 $this->serve();
282 }
283 }
284
285 /**
286 * Get Zoho CRM fields from WordPress database.
287 */
288 public function get_zcrm_fields() {
289 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Admin-only AJAX call, nonce verified in parent.
290 $module = isset( $_GET['module'] ) ? sanitize_text_field( wp_unslash( $_GET['module'] ) ) : '';
291 if ( empty( $module ) ) {
292 $this->errors['message'] = 'Module name is required.';
293 } else {
294 $fields = get_option( 'zcrm_' . strtolower( $module ) . '_fields', array() );
295 $this->response['fields'] = $fields;
296 $this->serve();
297 }
298 }
299
300 /**
301 * Retrieves the fields and serves the response.
302 *
303 * @return object
304 */
305 public function zcrm_get_custom_fields(): void {
306
307 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Admin-only AJAX call, nonce verified in parent.
308 $module = isset( $_GET['module'] ) ? sanitize_text_field( wp_unslash( $_GET['module'] ) ) : '';
309
310 if ( empty( $module ) ) {
311 $this->errors['message'] = 'Module name is required.';
312 } else {
313 $this->verify();
314 $option_name = 'zcrm_' . strtolower( $module ) . '_custom_fields';
315 $this->response['form'] = get_option( $option_name, array() );
316 $this->serve();
317 }
318 }
319
320 /**
321 * Sets the custom fields for the form.
322 *
323 * @return void
324 */
325 public function zcrm_save_custom_fields(): void {
326 $this->verify( self::FORMS['fields'] );
327 $module = isset( $this->data['module'] ) ? $this->data['module'] : '';
328 if ( empty( $module ) ) {
329 $this->errors['message'] = 'Module name is required.';
330 } else {
331 try {
332 $option_name = 'zcrm_' . strtolower( $module ) . '_custom_fields';
333 update_option( $option_name, $this->data['form'] );
334 $this->response = array( 'message' => 'saved' );
335 } catch ( Throwable $throwable ) {
336 $this->errors = array( 'message' => $throwable->getMessage() );
337 }
338 $this->serve();
339 }
340 }
341
342 /**
343 * Resets the fields.
344 *
345 * @return void
346 */
347 public function zcrm_reset_custom_fields(): void {
348 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Admin-only AJAX call, nonce verified in parent.
349 $module = isset( $_GET['module'] ) ? sanitize_text_field( wp_unslash( $_GET['module'] ) ) : '';
350 if ( empty( $module ) ) {
351 $this->errors['message'] = 'Module name is required.';
352 } else {
353 $this->verify();
354 $option_name = 'zcrm_' . strtolower( $module ) . '_custom_fields';
355 delete_option( $option_name );
356 $this->response = array( 'message' => 'Reset successfully!' );
357 $this->serve();
358 }
359 }
360
361 /**
362 * Export Orders to Zoho CRM.
363 *
364 * @param int $start_date_raw The start date.
365 * @param int $end_date_raw The end date.
366 */
367 public function export_order( $start_date_raw, $end_date_raw ) {
368 // $fd = fopen( __DIR__ . '/export_order.log', 'a+' );
369
370 $start_date = gmdate( 'Y-m-d H:i:s', $start_date_raw );
371 $end_date = gmdate( 'Y-m-d H:i:s', $end_date_raw );
372 // Define the order statuses to exclude.
373 $exclude_statuses = array( 'wc-failed', 'wc-cancelled', 'wc-checkout-draft' );
374 $posts_per_page = 20;
375 $paged = 1;
376 $max_execution_time = ini_get( 'max_execution_time' );
377
378 // Extend execution time for bulk operations.
379 if ( $max_execution_time > 0 && $max_execution_time < 300 ) {
380 set_time_limit( 300 ); // phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged -- Required for bulk order sync.
381
382 }
383
384 do {
385 // Reset execution time for each batch to prevent timeout.
386 if ( function_exists( 'set_time_limit' ) ) {
387 set_time_limit( 60 ); // phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged -- Required for bulk order sync.
388 }
389
390 // Query to get orders.
391 $args = array(
392 'date_created' => $start_date . '...' . $end_date,
393 'status' => array_diff( array_keys( wc_get_order_statuses() ), $exclude_statuses ),
394 'limit' => $posts_per_page,
395 'paged' => $paged,
396 'orderby' => 'date',
397 'order' => 'ASC',
398 'return' => 'ids',
399 'meta_query' => array(
400 array(
401 'key' => 'zcrm_sales_order_id',
402 'compare' => 'NOT EXISTS',
403 ),
404 ),
405 );
406 $orders = wc_get_orders( $args );
407 // Loop through orders and schedule them for Zoho CRM sync.
408 foreach ( $orders as $order_id ) {
409 // Use WC Action Scheduler to sync the order to Zoho CRM.
410 $existing_schedule = as_has_scheduled_action( 'sync_zcrm_order', array( $order_id ) );
411 if ( ! $existing_schedule ) {
412 as_schedule_single_action( time(), 'sync_zcrm_order', array( $order_id ) );
413 }
414 }
415
416 // Check if we have more orders to process before cleanup.
417 $has_more_orders = ! empty( $orders );
418
419 // Memory management - clean up variables.
420 unset( $orders, $args );
421
422 // Optional: garbage collection to free memory.
423 if ( function_exists( 'gc_collect_cycles' ) ) {
424 gc_collect_cycles();
425 }
426
427 // Increment the offset for the next batch.
428 ++$paged;
429
430 // Safety check to prevent infinite loops (max 1000 pages = 20,000 orders).
431 if ( $paged > 1000 ) {
432 break;
433 }
434 } while ( $has_more_orders );
435 // fclose( $fd );
436 }
437
438 /**
439 * AJAX endpoint to export WooCommerce orders to Zoho CRM.
440 */
441 public function order_export() {
442 $this->verify( self::FORMS['order'] );
443 if ( empty( $this->data ) || empty( $this->data['range'] ) ) {
444 $this->response['success'] = false;
445 $this->response['message'] = __( 'Select dates', 'commercebird' );
446 $this->serve();
447 }
448 // Set the date range to last 30 days.
449 $start_date = strtotime( $this->data['range'][0] );
450 $end_date = strtotime( $this->data['range'][1] );
451 $zoho_crm_access_token = get_option( 'cmbird_zoho_crm_access_token' );
452 if ( empty( $zoho_crm_access_token ) ) {
453 $this->response['success'] = false;
454 $this->response['message'] = __( 'Zoho CRM is not Connected.', 'commercebird' );
455 $this->serve();
456 }
457 $this->export_order( $start_date, $end_date );
458 $this->response['success'] = true;
459 $this->response['message'] = __( 'Exported', 'commercebird' );
460 $this->serve();
461 }
462
463 /**
464 * Retrieves an array of all tax rates from WooCommerce.
465 *
466 * @return array An array of all tax rates.
467 */
468 public function wc_taxes(): array {
469 $wc_tax_array = array();
470 $tax_classes = WC_Tax::get_tax_classes(); // Retrieve all tax classes.
471 if ( ! in_array( '', $tax_classes ) ) { // Make sure "Standard rate" (empty class name) is present.
472 array_unshift( $tax_classes, '' );
473 }
474
475 foreach ( $tax_classes as $tax_class ) { // For each tax class, get all rates.
476 $taxes = WC_Tax::get_rates_for_tax_class( $tax_class );
477
478 foreach ( $taxes as $key => $tax ) {
479 $taxarray = (array) $tax;
480 $taxarray['id'] = $key;
481 $wc_tax_array[] = $taxarray;
482 }
483 }
484
485 return $wc_tax_array;
486 }
487 /**
488 * Executes the wc_tax_collect function.
489 *
490 * This function verifies the current state, retrieves the taxes using the
491 * wc_taxes method, and serves the response.
492 */
493 public function wc_tax_collect(): void {
494 $this->verify();
495 $this->response = $this->wc_taxes();
496 $this->serve();
497 }
498
499 /**
500 * Retrieves the tax information.
501 *
502 * @return void
503 */
504 public function tax_get(): void {
505 $this->verify();
506 $this->response = array();
507 foreach ( $this->wc_taxes() as $tax ) {
508 $this->response['selectedTaxRates'][] = $tax['id'] . '^^' . get_option( 'cmbird_zoho_crm_tax_rate_' . $tax['id'] );
509 }
510 $this->serve();
511 }
512
513
514 /**
515 * Saves tax rates to the database.
516 */
517 public function tax_set(): void {
518 $this->verify( self::FORMS['tax'] );
519 $rates = array_filter( $this->data['selectedTaxRates'] );
520 try {
521 foreach ( $rates as $value ) {
522 $valarray = explode( '^^', $value );
523 update_option( 'cmbird_zoho_crm_tax_rate_' . $valarray[0], $valarray[1] );
524 }
525 $this->response = array( 'message' => 'Saved' );
526 } catch ( Throwable $throwable ) {
527 $this->errors = array( 'message' => $throwable->getMessage() );
528 }
529
530 $this->serve();
531 }
532
533 /**
534 * Resets the tax settings.
535 *
536 * This function verifies the tax settings and then deletes the options
537 * related to the tax rates
538 *
539 * @return void
540 */
541 public function tax_reset(): void {
542 $this->verify();
543 foreach ( $this->wc_taxes() as $tax ) {
544 delete_option( 'cmbird_zoho_crm_tax_rate_' . $tax['id'] );
545 }
546 $this->response = array( 'message' => 'Reset successfully!' );
547 $this->serve();
548 }
549
550 /**
551 * Retrieves the Zoho tax rates for the organization.
552 *
553 * This function verifies the request, retrieves the organization ID and URL
554 * from the options, constructs the URL for the API request, and executes
555 * the cURL call to fetch the tax rates. If the response contains the
556 * 'taxes' key, it sets the response to the tax rates. Otherwise, it sets
557 * the errors to a default message.
558 */
559 public function zoho_tax_rates_collect(): void {
560 $this->verify();
561 $zoho_crm_url = get_option( 'cmbird_zoho_crm_url' );
562 $url = $zoho_crm_url . 'crm/v7/org/taxes';
563 try {
564 $execute_curl_call_handle = new CMBIRD_API_Handler_Zoho();
565 $json = (array) $execute_curl_call_handle->execute_curl_call_get( $url );
566
567 // error_log('Data: ' . print_r($response, true));
568 if (
569 is_array( $json ) &&
570 isset( $json['org_taxes'] ) &&
571 isset( $json['org_taxes']->taxes )
572 ) {
573 $this->response = $json['org_taxes']->taxes;
574 } else {
575 $this->errors = array( 'message' => 'Something went wrong!' );
576 }
577 } catch ( Throwable $throwable ) {
578 $this->errors = array( 'message' => $throwable->getMessage() );
579 }
580 $this->serve();
581 }
582
583 /**
584 * Get Zoho CRM Sales Order layouts.
585 */
586 public function layouts_get(): void {
587 $this->verify();
588 $zoho_crm_url = get_option( 'cmbird_zoho_crm_url' );
589 $url = $zoho_crm_url . 'crm/v7/settings/layouts?module=Sales_Orders';
590 try {
591 $execute_curl_call_handle = new CMBIRD_API_Handler_Zoho();
592 $json = (array) $execute_curl_call_handle->execute_curl_call_get( $url );
593
594 if (
595 is_array( $json ) &&
596 isset( $json['layouts'] ) &&
597 is_array( $json['layouts'] )
598 ) {
599 // Format the layouts for frontend dropdown.
600 $formatted_layouts = array();
601 foreach ( $json['layouts'] as $layout ) {
602 if ( isset( $layout->id ) && isset( $layout->name ) ) {
603 $formatted_layouts[] = array(
604 'id' => $layout->id,
605 'name' => $layout->name,
606 'api_name' => $layout->api_name ?? '',
607 'status' => $layout->status ?? '',
608 );
609 }
610 }
611 $this->response = $formatted_layouts;
612 } else {
613 $this->errors = array( 'message' => 'Unable to fetch layouts!' );
614 }
615 } catch ( Throwable $throwable ) {
616 $this->errors = array( 'message' => $throwable->getMessage() );
617 }
618 $this->serve();
619 }
620
621 /**
622 * Save selected Zoho CRM Sales Order layout.
623 */
624 public function layout_save(): void {
625 $this->verify( array( 'layout_id' ) );
626 if ( empty( $this->data['layout_id'] ) ) {
627 $this->errors = array( 'message' => 'Layout ID is required!' );
628 $this->serve();
629 return;
630 }
631
632 $updated = update_option( self::OPTIONS['zcrm_sales_order_layout'], sanitize_text_field( $this->data['layout_id'] ) );
633
634 if ( $updated ) {
635 $this->response = array( 'message' => 'Layout saved successfully!' );
636 } else {
637 $this->errors = array( 'message' => 'Failed to save layout!' );
638 }
639 $this->serve();
640 }
641
642 /**
643 * Get selected Zoho CRM Sales Order layout.
644 */
645 public function layout_get_selected(): void {
646 $this->verify();
647 $saved_layout = get_option( self::OPTIONS['zcrm_sales_order_layout'], '' );
648 $this->response = array( 'selected_layout' => $saved_layout );
649 $this->serve();
650 }
651
652 /**
653 * Get order settings for Zoho CRM sync.
654 */
655 public function order_settings_get(): void {
656 $this->verify();
657 $sync_as_deals = get_option( 'cmbird_zcrm_sync_as_deals', false );
658 $this->response = array( 'syncAsDeals' => (bool) $sync_as_deals );
659 $this->serve();
660 }
661
662 /**
663 * Save order settings for Zoho CRM sync.
664 */
665 public function order_settings_save(): void {
666 $this->verify( self::FORMS['order_settings'] );
667 try {
668 $sync_as_deals = isset( $this->data['syncAsDeals'] ) ? (bool) $this->data['syncAsDeals'] : false;
669 update_option( 'cmbird_zcrm_sync_as_deals', $sync_as_deals );
670 $this->response = array(
671 'success' => true,
672 'message' => __( 'Order settings saved successfully!', 'commercebird' ),
673 );
674 } catch ( Throwable $throwable ) {
675 $this->errors = array( 'message' => $throwable->getMessage() );
676 }
677 $this->serve();
678 }
679
680 /**
681 * Get Zoho CRM Deals fields.
682 */
683 public function deal_fields_get(): void {
684 $this->verify();
685 $refresh = isset( $this->data['refresh'] ) && $this->data['refresh'];
686
687 $fields = get_option( 'zcrm_deals_fields', array() );
688
689 // If refresh is requested or no fields exist, try to refresh them from Zoho.
690 if ( $refresh || empty( $fields ) ) {
691 $sync = new ZohoCRMSync();
692 $fresh_fields = $sync->get_all_fields( 'Deals' );
693 if ( ! is_wp_error( $fresh_fields ) ) {
694 // Convert to simple key-value format, excluding hardcoded fields.
695 $formatted_fields = array();
696 foreach ( $fresh_fields as $field_key => $field_label ) {
697 // Skip hardcoded fields (Deal_Name, Amount, Currency).
698 if ( in_array( $field_key, array( 'Deal_Name', 'Amount', 'Currency' ) ) ) {
699 continue;
700 }
701 $formatted_fields[ $field_key ] = $field_label;
702 }
703
704 // Add empty option at the beginning.
705 $formatted_fields = array_merge( array( '' => 'Select a field...' ), $formatted_fields );
706
707 update_option( 'zcrm_deals_fields', $formatted_fields );
708 $fields = $formatted_fields;
709 }
710 } else {
711 // Ensure empty option exists.
712 if ( ! isset( $fields[''] ) ) {
713 $fields = array_merge( array( '' => 'Select a field...' ), $fields );
714 }
715 }
716
717 $this->response = array( 'fields' => $fields );
718 $this->serve();
719 }
720
721 /**
722 * Get deal field mapping settings.
723 */
724 public function deal_field_mapping_get(): void {
725 $this->verify();
726 $mapping = get_option( 'cmbird_zcrm_deal_field_mapping', array() );
727 $this->response = array( 'dealFields' => $mapping );
728 $this->serve();
729 }
730
731 /**
732 * Save deal field mapping settings.
733 */
734 public function deal_field_mapping_save(): void {
735 $this->verify( self::FORMS['order_field_mapping'] );
736 try {
737 $deal_fields = isset( $this->data['dealFields'] ) ? $this->data['dealFields'] : array();
738 update_option( 'cmbird_zcrm_deal_field_mapping', $deal_fields );
739 $this->response = array(
740 'success' => true,
741 'message' => __( 'Deal field mapping saved successfully!', 'commercebird' ),
742 );
743 } catch ( Throwable $throwable ) {
744 $this->errors = array( 'message' => $throwable->getMessage() );
745 }
746 $this->serve();
747 }
748
749 /**
750 * Get WooCommerce order statuses.
751 */
752 public function wc_order_statuses_get(): void {
753 $this->verify();
754 $order_statuses = wc_get_order_statuses();
755 $formatted_statuses = array();
756
757 // Add empty option.
758 $formatted_statuses[''] = 'Select order status...';
759
760 foreach ( $order_statuses as $status_key => $status_label ) {
761 $formatted_statuses[ $status_key ] = $status_label;
762 }
763
764 $this->response = array( 'statuses' => $formatted_statuses );
765 $this->serve();
766 }
767
768 /**
769 * Get Zoho Deal stages.
770 */
771 public function deal_stages_get(): void {
772 $this->verify();
773 $cached_stages = get_option( 'zcrm_deal_stages', array() );
774
775 // Check if we have cached stages and they're not empty.
776 if ( ! empty( $cached_stages ) && is_array( $cached_stages ) && count( $cached_stages ) > 1 ) {
777 $this->response = array( 'stages' => $cached_stages );
778 $this->serve();
779 return;
780 }
781
782 try {
783 // Directly get the Stage field pick list values without calling get_all_fields.
784 $zoho_crm_url = get_option( 'cmbird_zoho_crm_url' );
785 $url = $zoho_crm_url . 'crm/v7/settings/fields?module=Deals';
786 $execute_curl_call_handle = new CMBIRD_API_Handler_Zoho();
787 $json = $execute_curl_call_handle->execute_curl_call_get( $url );
788
789 $stage_options = array( '' => 'Select stage...' );
790
791 if ( ! is_wp_error( $json ) && ! empty( $json->fields ) ) {
792 foreach ( $json->fields as $field ) {
793 if ( $field->api_name === 'Stage' && ! empty( $field->pick_list_values ) ) {
794 foreach ( $field->pick_list_values as $option ) {
795 $stage_options[ $option->actual_value ] = $option->display_value;
796 }
797 break;
798 }
799 }
800 }
801
802 // Cache the stages.
803 update_option( 'zcrm_deal_stages', $stage_options );
804 $this->response = array( 'stages' => $stage_options );
805 } catch ( \Exception $e ) {
806 $this->response = array( 'stages' => array( '' => 'Select stage...' ) );
807 }
808
809 $this->serve();
810 }
811
812 /**
813 * Get order status to stage mapping settings.
814 */
815 public function order_status_stage_mapping_get(): void {
816 $this->verify();
817 $mapping = get_option( 'cmbird_zcrm_order_status_stage_mapping', array() );
818 $this->response = array( 'mapping' => $mapping );
819 $this->serve();
820 }
821
822 /**
823 * Save order status to stage mapping settings.
824 */
825 public function order_status_stage_mapping_save(): void {
826 $this->verify( self::FORMS['order_status_stage_mapping'] );
827 try {
828 $mapping = isset( $this->data['mapping'] ) ? $this->data['mapping'] : array();
829 update_option( 'cmbird_zcrm_order_status_stage_mapping', $mapping );
830 $this->response = array(
831 'success' => true,
832 'message' => __( 'Order status to stage mapping saved successfully!', 'commercebird' ),
833 );
834 } catch ( Throwable $throwable ) {
835 $this->errors = array( 'message' => $throwable->getMessage() );
836 }
837 $this->serve();
838 }
839
840 /**
841 * Export all existing WooCommerce customers to Zoho CRM as contacts.
842 */
843 public function contact_export(): void {
844 $this->verify();
845
846 $zoho_crm_access_token = get_option( 'cmbird_zoho_crm_access_token' );
847 if ( empty( $zoho_crm_access_token ) ) {
848 $this->response['success'] = false;
849 $this->response['message'] = __( 'Zoho CRM is not Connected.', 'commercebird' );
850 $this->serve();
851 }
852
853 try {
854 // Get all customers in batches.
855 $posts_per_page = 20;
856 $paged = 1;
857 $total_exported = 0;
858
859 do {
860 $customers = get_users(
861 array(
862 'role' => 'customer',
863 'number' => $posts_per_page,
864 'paged' => $paged,
865 'fields' => 'ID',
866 'orderby' => 'registered',
867 'order' => 'ASC',
868 )
869 );
870
871 // Loop through customers and schedule them for Zoho CRM sync.
872 foreach ( $customers as $customer_id ) {
873 // Use WC Action Scheduler to sync the customer to Zoho CRM.
874 $existing_schedule = as_has_scheduled_action( 'sync_zcrm_contact', array( $customer_id ) );
875 if ( ! $existing_schedule ) {
876 as_schedule_single_action( time(), 'sync_zcrm_contact', array( $customer_id ) );
877 ++$total_exported;
878 }
879 }
880
881 // Increment the page for the next batch.
882 ++$paged;
883 } while ( ! empty( $customers ) );
884
885 $this->response = array(
886 'success' => true,
887 'message' => sprintf(
888 /* translators: %d: number of customers */
889 __( '%d customers scheduled for export to Zoho CRM.', 'commercebird' ),
890 $total_exported
891 ),
892 );
893
894 } catch ( Throwable $throwable ) {
895 $this->errors = array( 'message' => $throwable->getMessage() );
896 }
897
898 $this->serve();
899 }
900
901 /**
902 * Get Zoho CRM Contact layouts.
903 */
904 public function contact_layouts_get(): void {
905 $this->verify();
906 $zoho_crm_url = get_option( 'cmbird_zoho_crm_url' );
907 $url = $zoho_crm_url . 'crm/v7/settings/layouts?module=Contacts';
908
909 try {
910 $execute_curl_call_handle = new CMBIRD_API_Handler_Zoho();
911 $json = (array) $execute_curl_call_handle->execute_curl_call_get( $url );
912
913 if (
914 is_array( $json ) &&
915 isset( $json['layouts'] ) &&
916 is_array( $json['layouts'] )
917 ) {
918 // Format the layouts for frontend dropdown.
919 $formatted_layouts = array();
920 foreach ( $json['layouts'] as $layout ) {
921 if ( isset( $layout->id ) && isset( $layout->name ) ) {
922 $formatted_layouts[] = array(
923 'id' => $layout->id,
924 'name' => $layout->name,
925 'api_name' => $layout->api_name ?? '',
926 'status' => $layout->status ?? '',
927 );
928 }
929 }
930 $this->response = array( 'layouts' => $formatted_layouts );
931 } else {
932 $this->errors = array( 'message' => 'Unable to fetch contact layouts!' );
933 }
934 } catch ( Throwable $throwable ) {
935 $this->errors = array( 'message' => $throwable->getMessage() );
936 }
937 $this->serve();
938 }
939
940 /**
941 * Save selected Zoho CRM Contact layout.
942 */
943 public function contact_layout_save(): void {
944 $this->verify( self::FORMS['contact_layout'] );
945
946 if ( empty( $this->data['layoutId'] ) ) {
947 $this->errors = array( 'message' => 'Contact Layout ID is required!' );
948 $this->serve();
949 return;
950 }
951
952 $updated = update_option( self::OPTIONS['zcrm_contact_layout'], sanitize_text_field( $this->data['layoutId'] ) );
953
954 if ( false !== $updated ) {
955 $this->response = array( 'message' => 'Contact layout saved successfully!' );
956 } else {
957 $this->errors = array( 'message' => 'Failed to save contact layout!' );
958 }
959 $this->serve();
960 }
961
962 /**
963 * Get selected Zoho CRM Contact layout.
964 */
965 public function contact_layout_get_selected(): void {
966 $this->verify();
967 $saved_layout = get_option( self::OPTIONS['zcrm_contact_layout'], '' );
968 $this->response = array( 'selectedLayout' => $saved_layout );
969 $this->serve();
970 }
971 }
972