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 / includes / woo-functions.php
commercebird / includes Last commit date
classes 2 months ago sync 6 months ago index.php 1 year ago woo-functions.php 2 months ago
woo-functions.php
979 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 $urlitem = "{$zoho_inventory_url}inventory/v1/items/{$zi_item_id}?organization_id=$zoho_inventory_oid";
484 // fwrite( $fd, PHP_EOL . 'URL : ' . $urlitem );
485
486 sleep( 1 ); // to avoid 429 error.
487 $execute_curl_call = new CMBIRD_API_Handler_Zoho();
488 $json = $execute_curl_call->execute_curl_call_get( $urlitem );
489 $code = (int) property_exists( $json, 'code' ) ? $json->code : '0';
490 if ( '0' === $code || 0 === $code ) {
491 // echo the item details here that are in array called "item".
492 $item = $json->item;
493 $actual_available_stock = (int) property_exists( $item, 'actual_available_stock' ) ? $item->actual_available_stock : '0';
494 $rate = (int) property_exists( $item, 'rate' ) ? $item->rate : '0';
495 echo '<p class="howto"><strong>Rate: </strong>' . esc_html( $rate ) . '</p>';
496 // echo the actual_available_stock.
497 echo '<p class="howto"><strong>Available Stock: </strong>' . esc_html( $actual_available_stock ) . '</p>';
498 // echo the zoho category name from the item.
499 $category_name = (string) property_exists( $item, 'category_name' ) ? $item->category_name : '';
500 if ( ! empty( $category_name ) || 'variable' === $product_type || 'variable-subscription' === $product_type ) {
501 echo '<p class="howto"><strong>Category Name: </strong>' . esc_html( $category_name ) . '</p>';
502 }
503 // echo all properties from item except description.
504 echo '<details><summary>More Details</summary>';
505 echo '<ul>';
506 foreach ( $item as $key => $value ) {
507 if ( 'description' === $key ) {
508 continue; // skip description.
509 }
510 // Skip empty arrays.
511 if ( is_array( $value ) && empty( $value ) ) {
512 continue;
513 }
514 echo '<li><strong>' . esc_html( $key ) . ': </strong>';
515 if ( is_array( $value ) || is_object( $value ) ) {
516 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r -- Debug display, output is escaped.
517 echo '<pre>' . esc_html( print_r( $value, true ) ) . '</pre>';
518 } else {
519 echo esc_html( $value );
520 }
521 echo '</li>';
522 }
523 echo '</ul>';
524 echo '</details>';
525 echo '<br>';
526 }
527 }
528 }
529
530 /**
531 * Callback for the CommerceBird AI metabox on product edit pages.
532 *
533 * @param WP_Post $post The current post object.
534 *
535 * @return void
536 */
537 function cmbird_product_ai_metabox_callback( $post ) {
538 $notice_nonce = isset( $_GET['cmbird_ai_review_notice_nonce'] ) ? sanitize_text_field( wp_unslash( $_GET['cmbird_ai_review_notice_nonce'] ) ) : '';
539 if ( $notice_nonce && wp_verify_nonce( $notice_nonce, 'cmbird_ai_review_notice_' . $post->ID ) ) {
540 $status_post_id = isset( $_GET['cmbird_ai_review_post'] ) ? absint( wp_unslash( $_GET['cmbird_ai_review_post'] ) ) : 0;
541 if ( $status_post_id === (int) $post->ID ) {
542 $status_type = isset( $_GET['cmbird_ai_review_status'] ) ? sanitize_key( wp_unslash( $_GET['cmbird_ai_review_status'] ) ) : '';
543 $message = isset( $_GET['cmbird_ai_review_message'] ) ? sanitize_text_field( wp_unslash( $_GET['cmbird_ai_review_message'] ) ) : '';
544 if ( ! empty( $message ) ) {
545 $bg_color = 'success' === $status_type ? '#ecfdf3' : '#fef2f2';
546 $fg_color = 'success' === $status_type ? '#166534' : '#b91c1c';
547 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>';
548 }
549 }
550 }
551
552 $ai_action_url = add_query_arg(
553 array(
554 'action' => 'cmbird_generate_ai_review_summary',
555 'post_id' => (int) $post->ID,
556 ),
557 admin_url( 'admin-post.php' )
558 );
559 $ai_action_url = wp_nonce_url( $ai_action_url, 'cmbird_generate_ai_review_summary_' . $post->ID, 'cmbird_ai_nonce' );
560
561 echo '<a href="' . esc_url( $ai_action_url ) . '" style="width:100%; text-align:center;" class="button button-primary">Generate Review Summary</a>';
562 }
563 add_action( 'add_meta_boxes', 'cmbird_product_metabox' );
564
565 /**
566 * Manually generate AI review summary for a product from edit screen.
567 *
568 * @return void
569 */
570 function cmbird_generate_ai_review_summary_action() {
571 $post_id = isset( $_GET['post_id'] ) ? absint( wp_unslash( $_GET['post_id'] ) ) : 0;
572
573 if ( 0 === $post_id ) {
574 wp_die( esc_html__( 'Invalid product.', 'commercebird' ) );
575 }
576
577 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 ) ) {
578 wp_die( esc_html__( 'Security check failed.', 'commercebird' ) );
579 }
580
581 if ( ! current_user_can( 'edit_post', $post_id ) ) {
582 wp_die( esc_html__( 'You do not have permission to edit this product.', 'commercebird' ) );
583 }
584
585 if ( 'product' !== get_post_type( $post_id ) ) {
586 wp_die( esc_html__( 'This action is only available for products.', 'commercebird' ) );
587 }
588
589 $message = '';
590 $status = 'error';
591
592 if ( ! (bool) get_option( 'cmbird_ai_review_summary_enabled', false ) ) {
593 $message = __( 'Enable AI Review Summaries in settings before generating.', 'commercebird' );
594 } elseif ( ! class_exists( '\\CommerceBird\\CMBIRD_AI_Product_Blocks_Generator' ) ) {
595 $message = __( 'Summary generator class is not available.', 'commercebird' );
596 } else {
597 $generator = new \CommerceBird\CMBIRD_AI_Product_Blocks_Generator();
598 if ( $generator->generate( $post_id ) ) {
599 $status = 'success';
600 $message = __( 'Summary generated successfully.', 'commercebird' );
601 } else {
602 $message = $generator->get_last_error_message();
603 if ( empty( $message ) ) {
604 $message = __( 'Failed to generate summary.', 'commercebird' );
605 }
606 }
607 }
608
609 $redirect_url = add_query_arg(
610 array(
611 'post' => $post_id,
612 'action' => 'edit',
613 'cmbird_ai_review_post' => $post_id,
614 'cmbird_ai_review_status' => $status,
615 'cmbird_ai_review_message' => $message,
616 'cmbird_ai_review_notice_nonce' => wp_create_nonce( 'cmbird_ai_review_notice_' . $post_id ),
617 ),
618 admin_url( 'post.php' )
619 );
620
621 wp_safe_redirect( $redirect_url );
622 exit;
623 }
624 add_action( 'admin_post_cmbird_generate_ai_review_summary', 'cmbird_generate_ai_review_summary_action' );
625
626
627 /**
628 * Add Zoho and Exact Item IDs to Product and Variations
629 *
630 * @return void
631 */
632 add_action( 'woocommerce_product_options_pricing', 'cmbird_item_id_field' );
633 add_action( 'woocommerce_variation_options_pricing', 'cmbird_item_id_variation_field', 10, 3 );
634 function cmbird_item_id_field() {
635 woocommerce_wp_text_input(
636 array(
637 'id' => 'eo_item_id',
638 'label' => esc_html__( 'Exact Item ID', 'commercebird' ),
639 'class' => 'readonly',
640 'desc_tip' => true,
641 'description' => esc_html__( 'This is the Exact Item ID of this product. You cannot change this', 'commercebird' ),
642 )
643 );
644 woocommerce_wp_text_input(
645 array(
646 'id' => 'zi_item_id',
647 'label' => esc_html__( 'Zoho Item ID', 'commercebird' ),
648 'class' => 'readonly',
649 'desc_tip' => true,
650 'description' => esc_html__( 'This is the Zoho Item ID of this product. You cannot change this', 'commercebird' ),
651 )
652 );
653 }
654 function cmbird_item_id_variation_field( $loop, $variation_data, $variation ) {
655 woocommerce_wp_text_input(
656 array(
657 'id' => 'eo_item_id[' . $loop . ']',
658 'class' => 'readonly',
659 'label' => esc_html__( 'Exact Item ID', 'commercebird' ),
660 'value' => get_post_meta( $variation->ID, 'eo_item_id', true ),
661 'desc_tip' => true,
662 'description' => esc_html__( 'This is the Exact Item ID of this product. You cannot change this', 'commercebird' ),
663 )
664 );
665 woocommerce_wp_text_input(
666 array(
667 'id' => 'zi_item_id[' . $loop . ']',
668 'class' => 'readonly',
669 'label' => esc_html__( 'Zoho Item ID', 'commercebird' ),
670 'value' => get_post_meta( $variation->ID, 'zi_item_id', true ),
671 'desc_tip' => true,
672 'description' => esc_html__( 'This is the Zoho Item ID of this product. You cannot change this', 'commercebird' ),
673 )
674 );
675 }
676
677 /**
678 * Adds 'Zoho Sync' column header to 'Orders' page immediately after 'Total' column.
679 *
680 * @param string[] $columns
681 * @return string[] $new_columns
682 */
683 function cmbird_zi_sync_column_orders_overview( $columns ) {
684 $zoho_inventory_access_token = get_option( 'cmbird_zoho_inventory_access_token' );
685 $zoho_crm_access_token = get_option( 'cmbird_zoho_crm_access_token' );
686 if ( empty( $zoho_inventory_access_token ) && empty( $zoho_crm_access_token ) ) {
687 return $columns;
688 }
689 $new_columns = array();
690 foreach ( $columns as $column_name => $column_info ) {
691
692 $new_columns[ $column_name ] = $column_info;
693
694 if ( 'order_total' === $column_name ) {
695 $new_columns['zoho_sync'] = __( 'Zoho Sync', 'commercebird' );
696 }
697 }
698 return $new_columns;
699 }
700 add_filter( 'manage_woocommerce_page_wc-orders_columns', 'cmbird_zi_sync_column_orders_overview', 20 );
701
702 /**
703 * Adding Sync Status for Orders Column
704 *
705 * @param string $column Column name.
706 * @param int $order_id $order id.
707 * @return void
708 */
709 function cmbird_zi_add_zoho_orders_content( $column, $order_id ) {
710 $zi_url = get_option( 'cmbird_zoho_inventory_url' );
711 $zi_visit_url = str_replace( 'www.zohoapis', 'inventory.zoho', $zi_url );
712 switch ( $column ) {
713 case 'zoho_sync':
714 // Get custom order meta data.
715 $order = wc_get_order( $order_id );
716 $zi_order_id = $order->get_meta( 'zi_salesorder_id', true, 'edit' );
717 $zcrm_order_id = $order->get_meta( 'zcrm_sales_order_id', true, 'edit' );
718
719 if ( $zi_order_id || $zcrm_order_id ) {
720 echo '<span class="dashicons dashicons-yes-alt" style="color:green;"></span>';
721
722 // Only show the external link if we have a ZI order ID for the URL.
723 if ( $zi_order_id ) {
724 $url = $zi_visit_url . 'app#/salesorders/' . $zi_order_id;
725 echo '<a href="' . esc_url( $url ) . '" target="_blank"> <span class="dashicons dashicons-external" style="color:green;"></span> </a>';
726 }
727 } else {
728 echo '<span class="dashicons dashicons-dismiss" style="color:red;"></span>';
729 }
730 unset( $order );
731 break;
732 }
733 }
734 add_action( 'manage_woocommerce_page_wc-orders_custom_column', 'cmbird_zi_add_zoho_orders_content', 20, 2 );
735
736 /**
737 * Adds 'Zoho Sync' column content.
738 *
739 * @param string[] $column name of column being displayed
740 */
741 function cmbird_zi_add_zoho_column_content( $column ) {
742 global $post;
743 $post_type = get_post_type( $post );
744
745 if ( 'zoho_sync' === $column && 'product' === $post_type ) {
746 $product_id = $post->ID;
747 $zi_product_id = get_post_meta( $product_id, 'zi_item_id' );
748 if ( $zi_product_id ) {
749 echo '<span class="dashicons dashicons-yes-alt" style="color:green;"></span>';
750 } else {
751 echo '<span class="dashicons dashicons-dismiss" style="color:red;"></span>';
752 }
753 }
754 }
755 add_action( 'manage_product_posts_custom_column', 'cmbird_zi_add_zoho_column_content' );
756
757 /**
758 * Adds 'Zoho Sync' column header to 'Products' page.
759 *
760 * @param string[] $columns
761 * @return string[] $new_columns
762 */
763 function cmbird_zi_sync_column_products_overview( $columns ) {
764 $zoho_inventory_access_token = get_option( 'cmbird_zoho_inventory_access_token' );
765 if ( empty( $zoho_inventory_access_token ) ) {
766 return $columns;
767 }
768 $new_columns = array();
769
770 foreach ( $columns as $column_name => $column_info ) {
771
772 $new_columns[ $column_name ] = $column_info;
773
774 if ( 'product_cat' === $column_name ) {
775 $new_columns['zoho_sync'] = __( 'Zoho Sync', 'commercebird' );
776 }
777 }
778
779 return $new_columns;
780 }
781 add_filter( 'manage_edit-product_columns', 'cmbird_zi_sync_column_products_overview', 20 );
782
783 /**
784 * Make 'Zoho Sync' column filterable.
785 */
786 function cmbird_zi_sync_column_filterable() {
787 global $typenow;
788
789 if ( 'product' === $typenow ) {
790 // code here.
791 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Admin product filter, no data modification.
792 $value = isset( $_GET['zoho_sync_filter'] ) ? sanitize_text_field( wp_unslash( $_GET['zoho_sync_filter'] ) ) : '';
793
794 echo '<select name="zoho_sync_filter">';
795 echo '<option value="">Zoho Sync Filter</option>';
796
797 // Count synced products.
798 $synced_count = new WP_Query(
799 array(
800 'post_type' => 'product',
801 'meta_query' => array(
802 array(
803 'key' => 'zi_item_id',
804 'compare' => 'EXISTS',
805 ),
806 ),
807 'fields' => 'ids',
808 )
809 );
810 $synced_count = $synced_count->found_posts;
811 $synced_label = 'Synced';
812 if ( $synced_count > 0 ) {
813 $synced_label .= ' (' . $synced_count . ')';
814 }
815 echo '<option value="synced" ' . selected( $value, 'synced', false ) . '>' . esc_html( $synced_label ) . '</option>';
816
817 // Count not synced products.
818 $not_synced_count = new WP_Query(
819 array(
820 'post_type' => 'product',
821 'meta_query' => array(
822 array(
823 'key' => 'zi_item_id',
824 'compare' => 'NOT EXISTS',
825 ),
826 ),
827 'fields' => 'ids',
828 )
829 );
830 $not_synced_count = $not_synced_count->found_posts;
831 $not_synced_label = 'Not Synced';
832 if ( $not_synced_count > 0 ) {
833 $not_synced_label .= ' (' . $not_synced_count . ')';
834 }
835 echo '<option value="not_synced" ' . selected( $value, 'not_synced', false ) . '>' . esc_html( $not_synced_label ) . '</option>';
836
837 echo '</select>';
838 }
839 }
840 add_action( 'restrict_manage_posts', 'cmbird_zi_sync_column_filterable' );
841
842 /**
843 * Modify the product query based on the filter.
844 *
845 * @param WP_Query $query The query object.
846 */
847 function cmbird_zi_sync_column_filter_query( $query ) {
848 global $typenow, $pagenow;
849
850 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Admin product filter query, no data modification.
851 if ( 'product' === $typenow && 'edit.php' === $pagenow && isset( $_GET['zoho_sync_filter'] ) && $_GET['zoho_sync_filter'] !== '' ) {
852 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Admin product filter query, no data modification.
853 $value = sanitize_text_field( wp_unslash( $_GET['zoho_sync_filter'] ) );
854
855 $meta_query = array();
856
857 if ( 'synced' === $value ) {
858 $meta_query[] = array(
859 'key' => 'zi_item_id',
860 'compare' => 'EXISTS',
861 );
862 } elseif ( 'not_synced' === $value ) {
863 $meta_query[] = array(
864 'relation' => 'OR',
865 array(
866 'key' => 'zi_item_id',
867 'compare' => 'NOT EXISTS',
868 ),
869 array(
870 'key' => 'zi_item_id',
871 'value' => '',
872 'compare' => '=',
873 ),
874 );
875 }
876
877 $query->set( 'meta_query', $meta_query );
878 }
879 }
880 add_action( 'pre_get_posts', 'cmbird_zi_sync_column_filter_query' );
881
882 /**
883 * Change Action Scheduler default purge to 1 week
884 *
885 * @return int
886 */
887 function cmbird_action_scheduler_purge() {
888 return WEEK_IN_SECONDS;
889 }
890 add_filter( 'action_scheduler_retention_period', 'cmbird_action_scheduler_purge' );
891
892 add_filter(
893 'action_scheduler_default_cleaner_statuses',
894 function ( $statuses ) {
895 $statuses[] = ActionScheduler_Store::STATUS_FAILED;
896 return $statuses;
897 }
898 );
899
900 /**
901 * Register the custom order statuses "shipped" and "fulfilled"
902 *
903 * @return void
904 */
905 function cmbird_register_custom_order_statuses() {
906 register_post_status(
907 'wc-shipped',
908 array(
909 'label' => _x( 'Shipped', 'Order status', 'commercebird' ),
910 'public' => true,
911 'exclude_from_search' => false,
912 'show_in_admin_all_list' => true,
913 'show_in_admin_status_list' => true,
914 // translators: %s is the number of shipped orders.
915 'label_count' => _n_noop( 'Shipped <span class="count">(%s)</span>', 'Shipped <span class="count">(%s)</span>', 'commercebird' ),
916 )
917 );
918 register_post_status(
919 'wc-fulfilled',
920 array(
921 'label' => _x( 'Fulfilled', 'Order status', 'commercebird' ),
922 'public' => true,
923 'exclude_from_search' => false,
924 'show_in_admin_all_list' => true,
925 'show_in_admin_status_list' => true,
926 // translators: %s is the number of fulfilled orders.
927 'label_count' => _n_noop( 'Fulfilled <span class="count">(%s)</span>', 'Fulfilled <span class="count">(%s)</span>', 'commercebird' ),
928 )
929 );
930 }
931 add_action( 'init', 'cmbird_register_custom_order_statuses' );
932 /**
933 * Add custom order statuses to list of WC Order statuses
934 *
935 * @param array $order_statuses Existing order statuses.
936 * @return array Modified order statuses.
937 */
938 function cmbird_add_custom_order_statuses( $order_statuses ) {
939 $order_statuses['wc-shipped'] = _x( 'Shipped', 'Order status', 'commercebird' );
940 $order_statuses['wc-fulfilled'] = _x( 'Fulfilled', 'Order status', 'commercebird' );
941 return $order_statuses;
942 }
943 add_filter( 'wc_order_statuses', 'cmbird_add_custom_order_statuses' );
944
945 /**
946 * Modify the Products API to include brands.
947 *
948 * TODO: This is a temporary solution. We need to find a better way to include brands in the API response.
949 *
950 * @param $response The response object.
951 * @param $post The post object.
952 * @param $request The request object.
953 * @return mixed
954 */
955 // add_filter( 'woocommerce_rest_prepare_product_object', 'cmbird_rest_api_prepare_brands_tax', 10, 3 );
956 // function cmbird_rest_api_prepare_brands_tax( $response, $object, $request ) {.
957 // $product_id = $object->get_id();
958 // if ( empty( $response->data['product_brands'] ) ) {.
959 // $terms = array();
960 // foreach ( wp_get_post_terms( $product_id, 'product_brands' ) as $term ) {.
961 // $terms[] = array(
962 // 'id' => $term->term_id,.
963 // 'name' => $term->name,.
964 // 'slug' => $term->slug,.
965 // );
966 // }.
967 // $response->data['product_brands'] = $terms;
968 // }.
969 // return $response;
970 // }.
971
972 // function cmbird_prepare_insert_product( $product, $request ) {.
973 // if ( isset( $request['product_brands'] ) ) {.
974 // wp_set_post_terms( $request['id'], $request['product_brands'], 'product_brands', false );
975 // }.
976 // return $product;
977 // }.
978 // add_filter( 'woocommerce_rest_pre_insert_product_object', 'cmbird_prepare_insert_product', 10, 2 );
979