PluginProbe ʕ •ᴥ•ʔ
CommerceBird – AI Command Center, ERP Integrations & B2B for WooCommerce (Zoho, Exact Online). / trunk
CommerceBird – AI Command Center, ERP Integrations & B2B for WooCommerce (Zoho, Exact Online). vtrunk
3.0.3 3.0.2 3.0.1 trunk 2.2.14 2.2.15 2.2.16 2.2.17 2.2.18 2.2.19 2.3.0 2.3.1 2.3.10 2.3.11 2.3.12 2.3.13 2.3.14 2.3.2 2.3.3 2.3.4 2.3.5 2.3.6 2.3.7 2.3.8 2.3.9 2.4.0 2.4.1 2.4.2 2.4.3 2.4.4 2.4.5 2.4.6 2.5.0 2.5.1 2.5.2 2.6.0 2.6.1 2.6.2 2.6.3 2.6.4 2.6.5 2.7.0 2.7.1 2.7.2 2.7.3 2.7.4 2.7.5 2.7.6 2.7.7 2.7.8 2.7.9 2.7.91 2.7.92 2.7.93 2.8.0 2.8.1 2.8.2 2.8.3 2.8.4 2.8.5 2.9.0 2.9.1 2.9.2 2.9.3 3.0.0
commercebird / includes / woo-functions.php
commercebird / includes Last commit date
classes 5 days ago sync 4 months ago index.php 1 year ago woo-functions.php 2 weeks ago
woo-functions.php
983 lines
1 <?php
2
3 /**
4 * All WooCommerce related functions.
5 *
6 * @category WooCommerce
7 * @package CommerceBird
8 * @author Fawad Tiemoerie <info@roadmapstudios.com>
9 * @license GNU General Public License v3.0
10 * @link https://commercebird.com
11 */
12
13 if ( ! defined( 'ABSPATH' ) ) {
14 exit;
15 }
16
17 /**
18 * Helper functions to ensure correct handling of Data being transferred via rest api.
19 *
20 * @param WC_Product $product The product object.
21 * @param WP_REST_Request $request The request object.
22 * @param boolean $is_creating True if the product is being created, false if it is being updated.
23 */
24 function cmbird_clear_product_cache( $product, $request, $is_creating ) {
25 if ( $is_creating ) {
26 $product_id = $product->get_id();
27 $zoho_inventory_access_token = get_option( 'cmbird_zoho_inventory_access_token' );
28 $cmbird_zi_product_sync = get_option( 'cmbird_zoho_disable_product_sync_status' );
29 if ( ! empty( $zoho_inventory_access_token ) && ! $cmbird_zi_product_sync ) {
30 // if request contain meta zi_item_id, then return.
31 if ( isset( $request['meta']['zi_item_id'] ) ) {
32 return;
33 }
34 $product_handler = new CMBIRD_Products_ZI_Export();
35 $product_handler->cmbird_zi_product_sync( $product_id );
36 }
37
38 // Sync to Zoho CRM as well.
39 $zoho_crm_access_token = get_option( 'cmbird_zoho_crm_access_token' );
40 if ( ! empty( $zoho_crm_access_token ) ) {
41 $cmbird_zcrm_product_sync = get_option( 'cmbird_zoho_crm_disable_product_sync_status' );
42 if ( ! $cmbird_zcrm_product_sync ) {
43 $product = wc_get_product( $product_id );
44 if ( $product ) {
45 $zcrm_product_handler = new CMBIRD_ZCRM_Product();
46 $zcrm_product_handler->cmbird_sync_product_to_zoho( $product );
47 }
48 }
49 }
50
51 wc_delete_product_transients( $product_id );
52 }
53 }
54 add_action( 'woocommerce_rest_insert_product_object', 'cmbird_clear_product_cache', 10, 3 );
55
56
57
58 /**
59 * Function to update the Contact in Zoho when customer updates address on frontend
60 *
61 * @param int $user_id - User ID.
62 */
63 function cmbird_update_contact_via_accountpage( $user_id ) {
64 // Return if this change is not made from the my account page.
65 if ( ! is_user_logged_in() ) {
66 return;
67 }
68 $zoho_inventory_access_token = get_option( 'cmbird_zoho_inventory_access_token' );
69 if ( ! empty( $zoho_inventory_access_token ) ) {
70 $contact_class_handle = new CMBIRD_Contact_ZI();
71 $contact_class_handle->cmbird_contact_update_function( $user_id );
72
73 global $wpdb;
74 $meta_key_search = 'zi_contact_person_id%';
75 $query = $wpdb->get_results(
76 $wpdb->prepare(
77 "SELECT meta_key, meta_value
78 FROM {$wpdb->usermeta}
79 WHERE user_id = %d
80 AND meta_key LIKE %s",
81 $user_id,
82 $meta_key_search
83 )
84 ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- User-specific meta query, not suitable for caching.
85 if ( ! empty( $query ) ) {
86 // Usermeta keys containing 'zi_contact_person_id' exist.
87 foreach ( $query as $row ) {
88 $contact_class_handle->cmbird_update_contact_person( $user_id, $row->meta_value );
89 }
90 } else {
91 return;
92 }
93 } else {
94 return;
95 }
96 }
97 add_action( 'profile_update', 'cmbird_update_contact_via_accountpage' );
98
99 add_action( 'wp_ajax_zoho_admin_product_sync', 'cmbird_zi_product_sync_class' );
100 /**
101 * Sync a product to Zoho Inventory and Zoho CRM.
102 *
103 * If the product is a variable product, it will sync all variations to Zoho Inventory and Zoho CRM.
104 * If the product is a simple product or variation, it will sync the product to Zoho Inventory and Zoho CRM.
105 *
106 * @param int $product_id The ID of the product to sync.
107 *
108 * @return void
109 */
110 function cmbird_zi_product_sync_class( $product_id ) {
111 if ( ! is_admin() ) {
112 return;
113 }
114
115 if ( ! $product_id ) {
116 // Check nonce for security.
117 if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'zoho_admin_product_sync' ) ) {
118 wp_send_json_error( 'Nonce verification failed' );
119 } else {
120 $product_id = isset( $_POST['post_id'] ) ? intval( $_POST['post_id'] ) : 0;
121 if ( ! $product_id ) {
122 wp_send_json_error( 'Invalid Product ID' );
123 }
124 }
125 }
126 $zoho_inventory_access_token = get_option( 'cmbird_zoho_inventory_access_token' );
127 $cmbird_zi_product_sync = get_option( 'cmbird_zoho_disable_product_sync_status' );
128 if ( ! $cmbird_zi_product_sync && $zoho_inventory_access_token ) {
129 $product_handler = new CMBIRD_Products_ZI_Export();
130 $product_handler->cmbird_zi_product_sync( $product_id );
131 }
132
133 // Sync to Zoho CRM as well.
134 $zoho_crm_access_token = get_option( 'cmbird_zoho_crm_access_token' );
135 if ( ! empty( $zoho_crm_access_token ) ) {
136 $cmbird_zcrm_product_sync = get_option( 'cmbird_zoho_crm_disable_product_sync_status' );
137 if ( ! $cmbird_zcrm_product_sync ) {
138 $product = wc_get_product( $product_id );
139 if ( $product ) {
140 $zcrm_product_handler = new CMBIRD_ZCRM_Product();
141 $sync_result = $zcrm_product_handler->cmbird_sync_product_to_zoho( $product );
142
143 // Handle different response types based on product type.
144 if ( $product->is_type( 'variable' ) && is_array( $sync_result ) ) {
145 // Variable product returns array with details.
146 $success_count = $sync_result['success_count'];
147 $failed_count = $sync_result['failed_count'];
148 $parent_id = $sync_result['parent_product_id'];
149
150 // If parent_id is not in result, get it from post meta (for existing products).
151 if ( ! $parent_id ) {
152 $parent_id = get_post_meta( $product_id, 'zcrm_product_id', true );
153 }
154
155 if ( $success_count > 0 ) {
156 update_post_meta( $product_id, 'zcrm_product_sync_status', 'success' );
157 $message = sprintf(
158 'Variable product synced: %d successful, %d failed. Parent ID: %s',
159 $success_count,
160 $failed_count,
161 $parent_id ? $parent_id : 'N/A'
162 );
163 update_post_meta( $product_id, 'zcrm_product_sync_message', $message );
164 } else {
165 update_post_meta( $product_id, 'zcrm_product_sync_status', 'failed' );
166 update_post_meta( $product_id, 'zcrm_product_sync_message', 'Failed to sync variable product and variations to Zoho CRM' );
167 }
168 } elseif ( $sync_result ) {
169 // Simple product or variation returns single ID.
170 update_post_meta( $product_id, 'zcrm_product_sync_status', 'success' );
171 update_post_meta( $product_id, 'zcrm_product_sync_message', 'Product synced successfully to Zoho CRM with ID: ' . $sync_result );
172 } else {
173 update_post_meta( $product_id, 'zcrm_product_sync_status', 'failed' );
174 update_post_meta( $product_id, 'zcrm_product_sync_message', 'Failed to sync product to Zoho CRM' );
175 }
176 }
177 }
178 }
179
180 // if its variable product but without variations, then sync it.
181 $product = wc_get_product( $product_id );
182 if ( $product->is_type( 'variable' ) ) {
183 $variations = $product->get_children();
184 if ( isset( $variations ) && count( $variations ) === 0 ) {
185 $zi_product_id = get_post_meta( $product_id, 'zi_item_id', true );
186 if ( ! empty( $zi_product_id ) ) {
187 // First get the item_group from Zoho Inventory using the zi_item_id and then sync the variations of that item group.
188 $get_call = new CMBIRD_API_Handler_Zoho();
189 $zoho_inventory_oid = get_option( 'cmbird_zoho_inventory_oid' );
190 if ( ! empty( $zoho_inventory_oid ) ) {
191 $zoho_inventory_url = get_option( 'cmbird_zoho_inventory_url' );
192 $url_item = "{$zoho_inventory_url}inventory/v1/itemgroups/{$zi_product_id}?organization_id=$zoho_inventory_oid";
193 $json = $get_call->execute_curl_call_get( $url_item );
194 $code = $json->code;
195 if ( is_wp_error( $json ) ) {
196 update_post_meta( $product_id, 'zi_product_errmsg', 'Error fetching item group from Zoho Inventory: ' . $json->get_error_message() );
197 return;
198 }
199 if ( 0 === $code || '0' === $code ) {
200 if ( isset( $json->item_group ) ) {
201 $product_handler = new CMBIRD_Products_ZI();
202 $product_handler->import_variable_product_variations( $json->item_group, $product_id );
203 }
204 }
205 }
206 }
207 }
208 }
209 }
210
211 /**
212 * Bulk-action to sync products from WooCommerce to Zoho
213 *
214 * @param array $bulk_array - array of bulk actions.
215 * @return array $bulk_array - array of bulk actions.
216 */
217 function cmbird_zi_sync_all_items_to_zoho( $bulk_array ) {
218 $bulk_array['sync_item_to_zoho'] = 'Sync to Zoho';
219 // add option to unmap items from zoho.
220 $bulk_array['unmap_item_to_zoho'] = 'Unmap from Zoho';
221 return $bulk_array;
222 }
223 add_filter( 'bulk_actions-edit-product', 'cmbird_zi_sync_all_items_to_zoho' );
224
225 add_filter( 'handle_bulk_actions-edit-product', 'cmbird_zi_sync_all_items_to_zoho_handler', 10, 3 );
226 function cmbird_zi_sync_all_items_to_zoho_handler( $redirect, $action, $object_ids ) {
227 // let's remove query args first.
228 $redirect = remove_query_arg( 'sync_item_to_zoho_done', $redirect );
229 $redirect = remove_query_arg( 'unmap_item_to_zoho_done', $redirect );
230
231 // do something for "Make Draft" bulk action.
232 if ( 'sync_item_to_zoho' === $action ) {
233
234 foreach ( $object_ids as $post_id ) {
235 // Sync to Zoho Inventory.
236 $product_handler = new CMBIRD_Products_ZI_Export();
237 $product_handler->cmbird_zi_product_sync( $post_id );
238
239 // Sync to Zoho CRM as well.
240 $zoho_crm_access_token = get_option( 'cmbird_zoho_crm_access_token' );
241 if ( ! empty( $zoho_crm_access_token ) ) {
242 $cmbird_zcrm_product_sync = get_option( 'cmbird_zoho_crm_disable_product_sync_status' );
243 if ( ! $cmbird_zcrm_product_sync ) {
244 $product = wc_get_product( $post_id );
245 if ( $product ) {
246 $zcrm_product_handler = new CMBIRD_ZCRM_Product();
247 $zcrm_product_handler->cmbird_sync_product_to_zoho( $product );
248 }
249 }
250 }
251 }
252
253 // do not forget to add query args to URL because we will show notices later.
254 $redirect = add_query_arg( 'sync_item_to_zoho_done', count( $object_ids ), $redirect );
255
256 } elseif ( 'unmap_item_to_zoho' === $action ) {
257 foreach ( $object_ids as $post_id ) {
258 $nonce = wp_create_nonce( 'zi_product_unmap_hook' );
259 cmbird_zi_product_unmap_hook( $post_id, $nonce );
260 }
261 // do not forget to add query args to URL because we will show notices later.
262 $redirect = add_query_arg( 'unmap_item_to_zoho_done', count( $object_ids ), $redirect );
263 }
264
265 return $redirect;
266 }
267
268 // output the message of bulk action.
269 add_action( 'admin_notices', 'cmbird_sync_item_to_zoho_notices' );
270 function cmbird_sync_item_to_zoho_notices() {
271 // verify nonce.
272 if ( ! isset( $_REQUEST['_wpnonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST['_wpnonce'] ) ), 'bulk-edit-products' ) ) {
273 return;
274 }
275 if ( ! empty( $_REQUEST['sync_item_to_zoho_done'] ) ) {
276 echo '<div id="message" class="updated notice is-dismissible">
277 <p>Products Synced. If product is not synced, please click on Edit Product to see the API response.</p>
278 </div>';
279 }
280 if ( ! empty( $_REQUEST['unmap_item_to_zoho_done'] ) ) {
281 echo '<div id="message" class="updated notice is-dismissible">
282 <p>Products Unmapped from Zoho.</p>
283 </div>';
284 }
285 }
286
287 /**
288 * Function to be called by ajax hook when unmap button called.
289 * This function remove zoho mapped id.
290 */
291 add_action( 'wp_ajax_zi_product_unmap_hook', 'cmbird_zi_product_unmap_hook' );
292 function cmbird_zi_product_unmap_hook( $product_id, $nonce = '' ) {
293 // if nonce is not there in the request, then check if it is passed as parameter.
294 if ( ! $nonce ) {
295 $nonce = isset( $_POST['nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] ) ) : '';
296 }
297 // verify nonce.
298 if ( ! $nonce || ! wp_verify_nonce( $nonce, 'zi_product_unmap_hook' ) ) {
299 wp_send_json_error( 'Nonce verification failed' );
300 }
301 if ( ! $product_id && isset( $_POST['product_id'] ) ) {
302 $product_id = sanitize_text_field( wp_unslash( $_POST['product_id'] ) );
303 }
304
305 if ( $product_id ) {
306 $product = wc_get_product( $product_id );
307 // If this is variable items then unmap all of it's variations.
308 if ( $product->is_type( 'variable' ) ) {
309 $variations = $product->get_children();
310 if ( isset( $variations ) && count( $variations ) > 0 ) {
311 foreach ( $variations as $child ) {
312 delete_post_meta( $child['variation_id'], 'zi_item_id' );
313 delete_post_meta( $child['variation_id'], 'zi_account_id' );
314 delete_post_meta( $child['variation_id'], 'zi_account_name' );
315 delete_post_meta( $child['variation_id'], 'zi_category_id' );
316 delete_post_meta( $child['variation_id'], 'zi_inventory_account_id' );
317 delete_post_meta( $child['variation_id'], 'zi_purchase_account_id' );
318 }
319 }
320 }
321 delete_post_meta( $product_id, 'zi_item_id' );
322 delete_post_meta( $product_id, 'zi_account_id' );
323 delete_post_meta( $product_id, 'zi_account_name' );
324 delete_post_meta( $product_id, 'zi_category_id' );
325 delete_post_meta( $product_id, 'zi_inventory_account_id' );
326 delete_post_meta( $product_id, 'zi_purchase_account_id' );
327
328 // Also unmap from Zoho CRM.
329 delete_post_meta( $product_id, 'zcrm_product_id' );
330 delete_post_meta( $product_id, 'zcrm_product_sync_status' );
331 delete_post_meta( $product_id, 'zcrm_product_sync_message' );
332
333 // update message.
334 update_post_meta( $product_id, 'zi_product_errmsg', 'Product is Unmapped from Zoho Inventory and CRM' );
335 }
336 }
337
338 /**
339 * Function to be called by ajax hook when unmap button called.
340 * This function remove zoho mapped id.
341 */
342 add_action( 'wp_ajax_zi_customer_unmap_hook', 'cmbird_zi_customer_unmap_hook' );
343 function cmbird_zi_customer_unmap_hook( $order_id ) {
344 // verify Nonce.
345 if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'zi_customer_unmap_hook' ) ) {
346 wp_send_json_error( 'Nonce verification failed' );
347 }
348 if ( ! $order_id && isset( $_POST['order_id'] ) ) {
349 $order_id = sanitize_text_field( wp_unslash( $_POST['order_id'] ) );
350 }
351
352 $order = wc_get_order( $order_id );
353 $customer_id = $order->get_user_id();
354
355 if ( $customer_id ) {
356 delete_user_meta( $customer_id, 'zi_contact_id' );
357 delete_user_meta( $customer_id, 'zi_contact_persons_id' );
358 delete_user_meta( $customer_id, 'zi_contactperson_id_0' );
359 delete_user_meta( $customer_id, 'zi_contactperson_id_1' );
360 delete_user_meta( $customer_id, 'zi_currency_code' );
361 delete_user_meta( $customer_id, 'zi_currency_id' );
362 delete_user_meta( $customer_id, 'zi_created_time' );
363 delete_user_meta( $customer_id, 'zi_last_modified_time' );
364 delete_user_meta( $customer_id, 'zi_primary_contact_id' );
365 delete_user_meta( $customer_id, 'zi_billing_address_id' );
366 delete_user_meta( $customer_id, 'zi_shipping_address_id' );
367
368 $order->add_order_note( 'Zoho Sync: Customer is now unmapped. Please try syncing the order again' );
369 $order->save();
370 }
371 }
372
373 /**
374 * Add WordPress Meta box to show sync response
375 */
376 function cmbird_product_metabox() {
377 add_meta_box(
378 'cmbird-ai-review-summary',
379 __( 'CommerceBird AI', 'commercebird' ),
380 'cmbird_product_ai_metabox_callback',
381 'product',
382 'side',
383 'high'
384 );
385
386 $zoho_inventory_access_token = get_option( 'cmbird_zoho_inventory_access_token' );
387 $zoho_crm_access_token = get_option( 'cmbird_zoho_crm_access_token' );
388 if ( ! $zoho_inventory_access_token && ! $zoho_crm_access_token ) {
389 return;
390 }
391 add_meta_box(
392 'zoho-product-sync',
393 __( 'Zoho Inventory', 'commercebird' ),
394 'cmbird_product_metabox_callback',
395 'product',
396 'side',
397 'high'
398 );
399 // normal metabox for zoho inventory body request.
400 add_meta_box(
401 'zoho-product-sync-normal',
402 __( 'Zoho Inventory - Full Details', 'commercebird' ),
403 'cmbird_product_metabox_body_request',
404 'product',
405 'normal',
406 'high'
407 );
408 }
409
410 /**
411 * Shows the full API request body when syncing the product to Zoho Inventory.
412 *
413 * @param WP_Post $post The current post object.
414 */
415 function cmbird_product_metabox_body_request( $post ) {
416 $body_request = get_post_meta( $post->ID, 'zi_product_body_request', true );
417
418 if ( empty( $body_request ) ) {
419 echo '<p>No API request body available. Try syncing the product first.</p>';
420 return;
421 }
422
423 // Format the JSON for better readability if it's valid JSON.
424 $formatted_json = $body_request;
425 $decoded = json_decode( $body_request, true );
426 if ( json_last_error() === JSON_ERROR_NONE ) {
427 $formatted_json = wp_json_encode( $decoded, JSON_PRETTY_PRINT );
428 }
429
430 echo '<h4>API Request Body (JSON):</h4>';
431 echo '<textarea readonly style="width:100%; height:400px; font-family:monospace; font-size:12px; border:1px solid # ccc; padding:10px;">' . esc_textarea( $formatted_json ) . '</textarea>';
432 echo '<p><small>This is the actual JSON data that was sent to Zoho Inventory API. You can copy this content.</small></p>';
433
434 // Add a copy button for convenience.
435 echo '<button type="button" onclick="copyBodyRequest()" style="margin-top:5px;" class="button">Copy to Clipboard</button>';
436 echo '<script>
437 function copyBodyRequest() {
438 const textarea = document.querySelector(\'textarea[readonly]\');
439 textarea.select();
440 document.execCommand(\'copy\');
441 alert(\'Request body copied to clipboard!\');
442 }
443 </script>';
444 }
445 /**
446 * Callback for product metabox to show sync response.
447 *
448 * @param WP_Post $post The current post object.
449 */
450 function cmbird_product_metabox_callback( $post ) {
451 $response = get_post_meta( $post->ID, 'zi_product_errmsg' );
452 echo 'API Response: ' . esc_html( implode( $response ) ) . '<br>';
453
454 // Show CRM sync status.
455 $zcrm_sync_status = get_post_meta( $post->ID, 'zcrm_product_sync_status', true );
456 $zcrm_sync_message = get_post_meta( $post->ID, 'zcrm_product_sync_message', true );
457 if ( $zcrm_sync_status ) {
458 $status_color = 'success' === $zcrm_sync_status ? 'green' : 'red';
459 echo '<p style="color:' . esc_attr( $status_color ) . ';"><strong>CRM Sync:</strong> ' . esc_html( $zcrm_sync_message ) . '</p>';
460 }
461
462 // Generate nonce.
463 $nonce = wp_create_nonce( 'zoho_admin_product_sync' );
464 $nonce_unmap = wp_create_nonce( 'zi_product_unmap_hook' );
465 $post_id = $post->ID;
466 echo '<br><a href="javascript:void(0)" style="width:100%; text-align: center;" class="button button-primary" onclick="zoho_admin_product_ajax(' . esc_attr( $post_id ) . ', \'' . esc_attr( $nonce ) . '\')">Sync Product</a>';
467 echo '<br><a href="javascript:void(0)" style="margin-top:10px; background:#b32d2e; border-color: # b32d2e; width:100%; text-align: center;" class="button button-primary" onclick="zoho_admin_unmap_product_ajax(' . esc_attr( $post_id ) . ', \'' . esc_attr( $nonce_unmap ) . '\')">Unmap this Product</a>';
468 $product = wc_get_product( $post->ID );
469 $product_type = $product->get_type();
470 if ( 'variable' === $product_type || 'variable-subscription' === $product_type ) {
471 echo '<p class="howto" style="color:#b32d2e;"><strong>Important : </strong> Please ensure all variations have price and SKU</p>';
472 }
473 // echo the zi_category_id.
474 $zi_category_id = get_post_meta( $post->ID, 'zi_category_id', true );
475 if ( $zi_category_id ) {
476 echo '<p class="howto"><strong>Zoho Category: </strong>' . esc_html( $zi_category_id ) . '</p>';
477 }
478 // make api call to get the zoho item details.
479 $zi_item_id = get_post_meta( $post->ID, 'zi_item_id', true );
480 if ( $zi_item_id && is_admin() ) {
481 $zoho_inventory_oid = get_option( 'cmbird_zoho_inventory_oid' );
482 $zoho_inventory_url = get_option( 'cmbird_zoho_inventory_url' );
483 if ( 'variable' === $product_type || 'variable-subscription' === $product_type ) {
484 $urlitem = "{$zoho_inventory_url}inventory/v1/itemgroups/{$zi_item_id}?organization_id=$zoho_inventory_oid";
485 } else {
486 $urlitem = "{$zoho_inventory_url}inventory/v1/items/{$zi_item_id}?organization_id=$zoho_inventory_oid";
487 }
488 // fwrite( $fd, PHP_EOL . 'URL : ' . $urlitem );
489
490 sleep( 1 ); // to avoid 429 error.
491 $execute_curl_call = new CMBIRD_API_Handler_Zoho();
492 $json = $execute_curl_call->execute_curl_call_get( $urlitem );
493 $code = (int) property_exists( $json, 'code' ) ? $json->code : '0';
494 if ( '0' === $code || 0 === $code ) {
495 // echo the item details here that are in array called "item" for non-variable products.
496 $item = ( 'variable' === $product_type || 'variable-subscription' === $product_type ) ? $json : $json->item;
497 $actual_available_stock = (int) property_exists( $item, 'actual_available_stock' ) ? $item->actual_available_stock : '0';
498 $rate = (int) property_exists( $item, 'rate' ) ? $item->rate : '0';
499 echo '<p class="howto"><strong>Rate: </strong>' . esc_html( $rate ) . '</p>';
500 // echo the actual_available_stock.
501 echo '<p class="howto"><strong>Available Stock: </strong>' . esc_html( $actual_available_stock ) . '</p>';
502 // echo the zoho category name from the item.
503 $category_name = (string) property_exists( $item, 'category_name' ) ? $item->category_name : '';
504 if ( ! empty( $category_name ) || 'variable' === $product_type || 'variable-subscription' === $product_type ) {
505 echo '<p class="howto"><strong>Category Name: </strong>' . esc_html( $category_name ) . '</p>';
506 }
507 // echo all properties from item except description.
508 echo '<details><summary>More Details</summary>';
509 echo '<ul>';
510 foreach ( $item as $key => $value ) {
511 if ( 'description' === $key ) {
512 continue; // skip description.
513 }
514 // Skip empty arrays.
515 if ( is_array( $value ) && empty( $value ) ) {
516 continue;
517 }
518 echo '<li><strong>' . esc_html( $key ) . ': </strong>';
519 if ( is_array( $value ) || is_object( $value ) ) {
520 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r -- Debug display, output is escaped.
521 echo '<pre>' . esc_html( print_r( $value, true ) ) . '</pre>';
522 } else {
523 echo esc_html( $value );
524 }
525 echo '</li>';
526 }
527 echo '</ul>';
528 echo '</details>';
529 echo '<br>';
530 }
531 }
532 }
533
534 /**
535 * Callback for the CommerceBird AI metabox on product edit pages.
536 *
537 * @param WP_Post $post The current post object.
538 *
539 * @return void
540 */
541 function cmbird_product_ai_metabox_callback( $post ) {
542 $notice_nonce = isset( $_GET['cmbird_ai_review_notice_nonce'] ) ? sanitize_text_field( wp_unslash( $_GET['cmbird_ai_review_notice_nonce'] ) ) : '';
543 if ( $notice_nonce && wp_verify_nonce( $notice_nonce, 'cmbird_ai_review_notice_' . $post->ID ) ) {
544 $status_post_id = isset( $_GET['cmbird_ai_review_post'] ) ? absint( wp_unslash( $_GET['cmbird_ai_review_post'] ) ) : 0;
545 if ( $status_post_id === (int) $post->ID ) {
546 $status_type = isset( $_GET['cmbird_ai_review_status'] ) ? sanitize_key( wp_unslash( $_GET['cmbird_ai_review_status'] ) ) : '';
547 $message = isset( $_GET['cmbird_ai_review_message'] ) ? sanitize_text_field( wp_unslash( $_GET['cmbird_ai_review_message'] ) ) : '';
548 if ( ! empty( $message ) ) {
549 $bg_color = 'success' === $status_type ? '#ecfdf3' : '#fef2f2';
550 $fg_color = 'success' === $status_type ? '#166534' : '#b91c1c';
551 echo '<p style="margin:0 0 10px;padding:8px 10px;border-radius:6px;background:' . esc_attr( $bg_color ) . ';color:' . esc_attr( $fg_color ) . ';"><strong>AI Review Summary:</strong> ' . esc_html( $message ) . '</p>';
552 }
553 }
554 }
555
556 $ai_action_url = add_query_arg(
557 array(
558 'action' => 'cmbird_generate_ai_review_summary',
559 'post_id' => (int) $post->ID,
560 ),
561 admin_url( 'admin-post.php' )
562 );
563 $ai_action_url = wp_nonce_url( $ai_action_url, 'cmbird_generate_ai_review_summary_' . $post->ID, 'cmbird_ai_nonce' );
564
565 echo '<a href="' . esc_url( $ai_action_url ) . '" style="width:100%; text-align:center;" class="button button-primary">Generate Review Summary</a>';
566 }
567 add_action( 'add_meta_boxes', 'cmbird_product_metabox' );
568
569 /**
570 * Manually generate AI review summary for a product from edit screen.
571 *
572 * @return void
573 */
574 function cmbird_generate_ai_review_summary_action() {
575 $post_id = isset( $_GET['post_id'] ) ? absint( wp_unslash( $_GET['post_id'] ) ) : 0;
576
577 if ( 0 === $post_id ) {
578 wp_die( esc_html__( 'Invalid product.', 'commercebird' ) );
579 }
580
581 if ( ! isset( $_GET['cmbird_ai_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['cmbird_ai_nonce'] ) ), 'cmbird_generate_ai_review_summary_' . $post_id ) ) {
582 wp_die( esc_html__( 'Security check failed.', 'commercebird' ) );
583 }
584
585 if ( ! current_user_can( 'edit_post', $post_id ) ) {
586 wp_die( esc_html__( 'You do not have permission to edit this product.', 'commercebird' ) );
587 }
588
589 if ( 'product' !== get_post_type( $post_id ) ) {
590 wp_die( esc_html__( 'This action is only available for products.', 'commercebird' ) );
591 }
592
593 $message = '';
594 $status = 'error';
595
596 if ( ! (bool) get_option( 'cmbird_ai_review_summary_enabled', false ) ) {
597 $message = __( 'Enable AI Review Summaries in settings before generating.', 'commercebird' );
598 } elseif ( ! class_exists( '\\CommerceBird\\CMBIRD_AI_Product_Blocks_Generator' ) ) {
599 $message = __( 'Summary generator class is not available.', 'commercebird' );
600 } else {
601 $generator = new \CommerceBird\CMBIRD_AI_Product_Blocks_Generator();
602 if ( $generator->generate( $post_id ) ) {
603 $status = 'success';
604 $message = __( 'Summary generated successfully.', 'commercebird' );
605 } else {
606 $message = $generator->get_last_error_message();
607 if ( empty( $message ) ) {
608 $message = __( 'Failed to generate summary.', 'commercebird' );
609 }
610 }
611 }
612
613 $redirect_url = add_query_arg(
614 array(
615 'post' => $post_id,
616 'action' => 'edit',
617 'cmbird_ai_review_post' => $post_id,
618 'cmbird_ai_review_status' => $status,
619 'cmbird_ai_review_message' => $message,
620 'cmbird_ai_review_notice_nonce' => wp_create_nonce( 'cmbird_ai_review_notice_' . $post_id ),
621 ),
622 admin_url( 'post.php' )
623 );
624
625 wp_safe_redirect( $redirect_url );
626 exit;
627 }
628 add_action( 'admin_post_cmbird_generate_ai_review_summary', 'cmbird_generate_ai_review_summary_action' );
629
630
631 /**
632 * Add Zoho and Exact Item IDs to Product and Variations
633 *
634 * @return void
635 */
636 add_action( 'woocommerce_product_options_pricing', 'cmbird_item_id_field' );
637 add_action( 'woocommerce_variation_options_pricing', 'cmbird_item_id_variation_field', 10, 3 );
638 function cmbird_item_id_field() {
639 woocommerce_wp_text_input(
640 array(
641 'id' => 'eo_item_id',
642 'label' => esc_html__( 'Exact Item ID', 'commercebird' ),
643 'class' => 'readonly',
644 'desc_tip' => true,
645 'description' => esc_html__( 'This is the Exact Item ID of this product. You cannot change this', 'commercebird' ),
646 )
647 );
648 woocommerce_wp_text_input(
649 array(
650 'id' => 'zi_item_id',
651 'label' => esc_html__( 'Zoho Item ID', 'commercebird' ),
652 'class' => 'readonly',
653 'desc_tip' => true,
654 'description' => esc_html__( 'This is the Zoho Item ID of this product. You cannot change this', 'commercebird' ),
655 )
656 );
657 }
658 function cmbird_item_id_variation_field( $loop, $variation_data, $variation ) {
659 woocommerce_wp_text_input(
660 array(
661 'id' => 'eo_item_id[' . $loop . ']',
662 'class' => 'readonly',
663 'label' => esc_html__( 'Exact Item ID', 'commercebird' ),
664 'value' => get_post_meta( $variation->ID, 'eo_item_id', true ),
665 'desc_tip' => true,
666 'description' => esc_html__( 'This is the Exact Item ID of this product. You cannot change this', 'commercebird' ),
667 )
668 );
669 woocommerce_wp_text_input(
670 array(
671 'id' => 'zi_item_id[' . $loop . ']',
672 'class' => 'readonly',
673 'label' => esc_html__( 'Zoho Item ID', 'commercebird' ),
674 'value' => get_post_meta( $variation->ID, 'zi_item_id', true ),
675 'desc_tip' => true,
676 'description' => esc_html__( 'This is the Zoho Item ID of this product. You cannot change this', 'commercebird' ),
677 )
678 );
679 }
680
681 /**
682 * Adds 'Zoho Sync' column header to 'Orders' page immediately after 'Total' column.
683 *
684 * @param string[] $columns
685 * @return string[] $new_columns
686 */
687 function cmbird_zi_sync_column_orders_overview( $columns ) {
688 $zoho_inventory_access_token = get_option( 'cmbird_zoho_inventory_access_token' );
689 $zoho_crm_access_token = get_option( 'cmbird_zoho_crm_access_token' );
690 if ( empty( $zoho_inventory_access_token ) && empty( $zoho_crm_access_token ) ) {
691 return $columns;
692 }
693 $new_columns = array();
694 foreach ( $columns as $column_name => $column_info ) {
695
696 $new_columns[ $column_name ] = $column_info;
697
698 if ( 'order_total' === $column_name ) {
699 $new_columns['zoho_sync'] = __( 'Zoho Sync', 'commercebird' );
700 }
701 }
702 return $new_columns;
703 }
704 add_filter( 'manage_woocommerce_page_wc-orders_columns', 'cmbird_zi_sync_column_orders_overview', 20 );
705
706 /**
707 * Adding Sync Status for Orders Column
708 *
709 * @param string $column Column name.
710 * @param int $order_id $order id.
711 * @return void
712 */
713 function cmbird_zi_add_zoho_orders_content( $column, $order_id ) {
714 $zi_url = get_option( 'cmbird_zoho_inventory_url' );
715 $zi_visit_url = str_replace( 'www.zohoapis', 'inventory.zoho', $zi_url );
716 switch ( $column ) {
717 case 'zoho_sync':
718 // Get custom order meta data.
719 $order = wc_get_order( $order_id );
720 $zi_order_id = $order->get_meta( 'zi_salesorder_id', true, 'edit' );
721 $zcrm_order_id = $order->get_meta( 'zcrm_sales_order_id', true, 'edit' );
722
723 if ( $zi_order_id || $zcrm_order_id ) {
724 echo '<span class="dashicons dashicons-yes-alt" style="color:green;"></span>';
725
726 // Only show the external link if we have a ZI order ID for the URL.
727 if ( $zi_order_id ) {
728 $url = $zi_visit_url . 'app#/salesorders/' . $zi_order_id;
729 echo '<a href="' . esc_url( $url ) . '" target="_blank"> <span class="dashicons dashicons-external" style="color:green;"></span> </a>';
730 }
731 } else {
732 echo '<span class="dashicons dashicons-dismiss" style="color:red;"></span>';
733 }
734 unset( $order );
735 break;
736 }
737 }
738 add_action( 'manage_woocommerce_page_wc-orders_custom_column', 'cmbird_zi_add_zoho_orders_content', 20, 2 );
739
740 /**
741 * Adds 'Zoho Sync' column content.
742 *
743 * @param string[] $column name of column being displayed
744 */
745 function cmbird_zi_add_zoho_column_content( $column ) {
746 global $post;
747 $post_type = get_post_type( $post );
748
749 if ( 'zoho_sync' === $column && 'product' === $post_type ) {
750 $product_id = $post->ID;
751 $zi_product_id = get_post_meta( $product_id, 'zi_item_id' );
752 if ( $zi_product_id ) {
753 echo '<span class="dashicons dashicons-yes-alt" style="color:green;"></span>';
754 } else {
755 echo '<span class="dashicons dashicons-dismiss" style="color:red;"></span>';
756 }
757 }
758 }
759 add_action( 'manage_product_posts_custom_column', 'cmbird_zi_add_zoho_column_content' );
760
761 /**
762 * Adds 'Zoho Sync' column header to 'Products' page.
763 *
764 * @param string[] $columns
765 * @return string[] $new_columns
766 */
767 function cmbird_zi_sync_column_products_overview( $columns ) {
768 $zoho_inventory_access_token = get_option( 'cmbird_zoho_inventory_access_token' );
769 if ( empty( $zoho_inventory_access_token ) ) {
770 return $columns;
771 }
772 $new_columns = array();
773
774 foreach ( $columns as $column_name => $column_info ) {
775
776 $new_columns[ $column_name ] = $column_info;
777
778 if ( 'product_cat' === $column_name ) {
779 $new_columns['zoho_sync'] = __( 'Zoho Sync', 'commercebird' );
780 }
781 }
782
783 return $new_columns;
784 }
785 add_filter( 'manage_edit-product_columns', 'cmbird_zi_sync_column_products_overview', 20 );
786
787 /**
788 * Make 'Zoho Sync' column filterable.
789 */
790 function cmbird_zi_sync_column_filterable() {
791 global $typenow;
792
793 if ( 'product' === $typenow ) {
794 // code here.
795 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Admin product filter, no data modification.
796 $value = isset( $_GET['zoho_sync_filter'] ) ? sanitize_text_field( wp_unslash( $_GET['zoho_sync_filter'] ) ) : '';
797
798 echo '<select name="zoho_sync_filter">';
799 echo '<option value="">Zoho Sync Filter</option>';
800
801 // Count synced products.
802 $synced_count = new WP_Query(
803 array(
804 'post_type' => 'product',
805 'meta_query' => array(
806 array(
807 'key' => 'zi_item_id',
808 'compare' => 'EXISTS',
809 ),
810 ),
811 'fields' => 'ids',
812 )
813 );
814 $synced_count = $synced_count->found_posts;
815 $synced_label = 'Synced';
816 if ( $synced_count > 0 ) {
817 $synced_label .= ' (' . $synced_count . ')';
818 }
819 echo '<option value="synced" ' . selected( $value, 'synced', false ) . '>' . esc_html( $synced_label ) . '</option>';
820
821 // Count not synced products.
822 $not_synced_count = new WP_Query(
823 array(
824 'post_type' => 'product',
825 'meta_query' => array(
826 array(
827 'key' => 'zi_item_id',
828 'compare' => 'NOT EXISTS',
829 ),
830 ),
831 'fields' => 'ids',
832 )
833 );
834 $not_synced_count = $not_synced_count->found_posts;
835 $not_synced_label = 'Not Synced';
836 if ( $not_synced_count > 0 ) {
837 $not_synced_label .= ' (' . $not_synced_count . ')';
838 }
839 echo '<option value="not_synced" ' . selected( $value, 'not_synced', false ) . '>' . esc_html( $not_synced_label ) . '</option>';
840
841 echo '</select>';
842 }
843 }
844 add_action( 'restrict_manage_posts', 'cmbird_zi_sync_column_filterable' );
845
846 /**
847 * Modify the product query based on the filter.
848 *
849 * @param WP_Query $query The query object.
850 */
851 function cmbird_zi_sync_column_filter_query( $query ) {
852 global $typenow, $pagenow;
853
854 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Admin product filter query, no data modification.
855 if ( 'product' === $typenow && 'edit.php' === $pagenow && isset( $_GET['zoho_sync_filter'] ) && $_GET['zoho_sync_filter'] !== '' ) {
856 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Admin product filter query, no data modification.
857 $value = sanitize_text_field( wp_unslash( $_GET['zoho_sync_filter'] ) );
858
859 $meta_query = array();
860
861 if ( 'synced' === $value ) {
862 $meta_query[] = array(
863 'key' => 'zi_item_id',
864 'compare' => 'EXISTS',
865 );
866 } elseif ( 'not_synced' === $value ) {
867 $meta_query[] = array(
868 'relation' => 'OR',
869 array(
870 'key' => 'zi_item_id',
871 'compare' => 'NOT EXISTS',
872 ),
873 array(
874 'key' => 'zi_item_id',
875 'value' => '',
876 'compare' => '=',
877 ),
878 );
879 }
880
881 $query->set( 'meta_query', $meta_query );
882 }
883 }
884 add_action( 'pre_get_posts', 'cmbird_zi_sync_column_filter_query' );
885
886 /**
887 * Change Action Scheduler default purge to 1 week
888 *
889 * @return int
890 */
891 function cmbird_action_scheduler_purge() {
892 return WEEK_IN_SECONDS;
893 }
894 add_filter( 'action_scheduler_retention_period', 'cmbird_action_scheduler_purge' );
895
896 add_filter(
897 'action_scheduler_default_cleaner_statuses',
898 function ( $statuses ) {
899 $statuses[] = ActionScheduler_Store::STATUS_FAILED;
900 return $statuses;
901 }
902 );
903
904 /**
905 * Register the custom order statuses "shipped" and "fulfilled"
906 *
907 * @return void
908 */
909 function cmbird_register_custom_order_statuses() {
910 register_post_status(
911 'wc-shipped',
912 array(
913 'label' => _x( 'Shipped', 'Order status', 'commercebird' ),
914 'public' => true,
915 'exclude_from_search' => false,
916 'show_in_admin_all_list' => true,
917 'show_in_admin_status_list' => true,
918 // translators: %s is the number of shipped orders.
919 'label_count' => _n_noop( 'Shipped <span class="count">(%s)</span>', 'Shipped <span class="count">(%s)</span>', 'commercebird' ),
920 )
921 );
922 register_post_status(
923 'wc-fulfilled',
924 array(
925 'label' => _x( 'Fulfilled', 'Order status', 'commercebird' ),
926 'public' => true,
927 'exclude_from_search' => false,
928 'show_in_admin_all_list' => true,
929 'show_in_admin_status_list' => true,
930 // translators: %s is the number of fulfilled orders.
931 'label_count' => _n_noop( 'Fulfilled <span class="count">(%s)</span>', 'Fulfilled <span class="count">(%s)</span>', 'commercebird' ),
932 )
933 );
934 }
935 add_action( 'init', 'cmbird_register_custom_order_statuses' );
936 /**
937 * Add custom order statuses to list of WC Order statuses
938 *
939 * @param array $order_statuses Existing order statuses.
940 * @return array Modified order statuses.
941 */
942 function cmbird_add_custom_order_statuses( $order_statuses ) {
943 $order_statuses['wc-shipped'] = _x( 'Shipped', 'Order status', 'commercebird' );
944 $order_statuses['wc-fulfilled'] = _x( 'Fulfilled', 'Order status', 'commercebird' );
945 return $order_statuses;
946 }
947 add_filter( 'wc_order_statuses', 'cmbird_add_custom_order_statuses' );
948
949 /**
950 * Modify the Products API to include brands.
951 *
952 * TODO: This is a temporary solution. We need to find a better way to include brands in the API response.
953 *
954 * @param $response The response object.
955 * @param $post The post object.
956 * @param $request The request object.
957 * @return mixed
958 */
959 // add_filter( 'woocommerce_rest_prepare_product_object', 'cmbird_rest_api_prepare_brands_tax', 10, 3 );
960 // function cmbird_rest_api_prepare_brands_tax( $response, $object, $request ) {.
961 // $product_id = $object->get_id();
962 // if ( empty( $response->data['product_brands'] ) ) {.
963 // $terms = array();
964 // foreach ( wp_get_post_terms( $product_id, 'product_brands' ) as $term ) {.
965 // $terms[] = array(
966 // 'id' => $term->term_id,.
967 // 'name' => $term->name,.
968 // 'slug' => $term->slug,.
969 // );
970 // }.
971 // $response->data['product_brands'] = $terms;
972 // }.
973 // return $response;
974 // }.
975
976 // function cmbird_prepare_insert_product( $product, $request ) {.
977 // if ( isset( $request['product_brands'] ) ) {.
978 // wp_set_post_terms( $request['id'], $request['product_brands'], 'product_brands', false );
979 // }.
980 // return $product;
981 // }.
982 // add_filter( 'woocommerce_rest_pre_insert_product_object', 'cmbird_prepare_insert_product', 10, 2 );
983