PluginProbe ʕ •ᴥ•ʔ
CommerceBird – AI Command Center, ERP Integrations & B2B for WooCommerce (Zoho, Exact Online). / 2.9.2
CommerceBird – AI Command Center, ERP Integrations & B2B for WooCommerce (Zoho, Exact Online). v2.9.2
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 / classes / class-common.php
commercebird / includes / classes Last commit date
apis 1 month ago purchase-orders 1 month ago quotes 1 month ago zoho-crm 6 months ago zoho-inventory 1 month ago class-api-handler-zoho.php 1 month ago class-auth-zoho.php 3 months ago class-common.php 1 month ago class-mcp.php 2 months ago class-plugin.php 1 month ago class-wc-api.php 5 months ago index.php 1 year ago
class-common.php
356 lines
1 <?php
2
3 if ( ! defined( 'ABSPATH' ) ) {
4 exit;
5 }
6
7 if ( ! class_exists( 'CMBIRD_Common_Functions' ) ) {
8 /**
9 * Common functions for CommerceBird plugin integration with Zoho.
10 */
11 class CMBIRD_Common_Functions {
12
13 /**
14 * Detect whether a WooCommerce B2B plugin that owns the `wcb2b_group` CPT is active.
15 *
16 * Different distributions of "WooCommerce B2B" ship under different class names
17 * (`WooCommerceB2B`, `WooCommerce_B2B`, …) and the original `class_exists('WooCommerceB2B')`
18 * gate silently disabled every B2B price-list code path on installs that used a different name.
19 * The post-type registration is the actual contract we depend on, so detect by any signal.
20 *
21 * Must be called after the `init` action (post types are registered there).
22 *
23 * @return bool
24 */
25 public static function is_wcb2b_active(): bool {
26 static $active = null;
27 if ( null !== $active ) {
28 return $active;
29 }
30 $active = class_exists( 'WooCommerceB2B' )
31 || class_exists( 'WooCommerce_B2B' )
32 || function_exists( 'WCB2B' )
33 || defined( 'WCB2B_VERSION' )
34 || defined( 'WCB2B_PLUGIN_VERSION' )
35 || post_type_exists( 'wcb2b_group' );
36 return $active;
37 }
38
39 /**
40 * Constructor.
41 */
42 public function __construct() {
43 add_action( 'woocommerce_thankyou', array( $this, 'cmbird_sync_frontend_order' ) );
44 add_action( 'woocommerce_rest_insert_shop_order_object', array( $this, 'cmbird_on_insert_rest_api' ), 20, 3 );
45 add_filter( 'wcs_renewal_order_created', array( $this, 'cmbird_zi_sync_renewal_order' ), 10, 2 );
46 add_action( 'wp_ajax_zoho_admin_order_sync', array( $this, 'cmbird_zoho_order_sync' ) );
47 }
48
49 /**
50 * Sync order when it's created via the checkout.
51 *
52 * @param int $order_id The order ID.
53 * @return void
54 */
55 public function cmbird_sync_frontend_order( $order_id ) {
56 // return if the order is not coming via thank you page.
57 if ( ! is_wc_endpoint_url( 'order-received' ) ) {
58 return;
59 }
60 // Check if the transient flag is set.
61 if ( get_transient( 'cmbird_thankyou_callback_executed_' . $order_id ) ) {
62 return;
63 }
64 $zoho_inventory_access_token = get_option( 'cmbird_zoho_inventory_access_token' );
65 // First sync the customer to Zoho Inventory if the access token is set.
66 if ( is_string( $zoho_inventory_access_token ) && trim( $zoho_inventory_access_token ) !== '' ) {
67 $zi_order_class = new CMBIRD_Order_Sync_ZI();
68 $zi_order_class->cmbird_zi_sync_customer_checkout( $order_id );
69 // Use WC Action Scheduler to sync the order to Zoho Inventory.
70 $existing_schedule = as_has_scheduled_action( 'sync_zi_order', array( $order_id ) );
71 if ( ! $existing_schedule ) {
72 as_schedule_single_action( time(), 'sync_zi_order', array( $order_id ) );
73 // Set the transient flag to prevent multiple executions.
74 set_transient( 'cmbird_thankyou_callback_executed_' . $order_id, true, 60 );
75 }
76 }
77 $zoho_crm_access_token = get_option( 'cmbird_zoho_crm_access_token' );
78 // If the access token is set, sync the order to Zoho CRM.
79 if ( is_string( $zoho_crm_access_token ) && trim( $zoho_crm_access_token ) !== '' ) {
80 // Use WC Action Scheduler to sync the order to Zoho CRM.
81 $existing_schedule = as_has_scheduled_action( 'sync_zcrm_order', array( $order_id ) );
82 if ( ! $existing_schedule ) {
83 as_schedule_single_action( time(), 'sync_zcrm_order', array( $order_id ) );
84 // Set the transient flag to prevent multiple executions.
85 set_transient( 'cmbird_thankyou_callback_executed_' . $order_id, true, 60 );
86 }
87 }
88 }
89
90 /**
91 * Sync order when its scheduled via the Action Scheduler.
92 *
93 * @return void
94 */
95 public function cmbird_orders_prepare_sync() {
96 $args = func_get_args();
97 $order_id = $args[0] ?? null;
98 if ( get_option( 'cmbird_zoho_inventory_access_token' ) && $order_id ) {
99 try {
100 $zi_order_class = new CMBIRD_Order_Sync_ZI();
101 $zi_order_class->zi_order_sync( $order_id );
102 } catch ( \Throwable $e ) {
103 error_log( 'Error in cmbird_orders_prepare_sync: ' . $e->getMessage() ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Required for security logging.
104 }
105 }
106 if ( get_option( 'cmbird_zoho_crm_access_token' ) && $order_id ) {
107 try {
108 $zcrm_order_class = new CMBIRD_ZCRM_SalesOrder();
109 $zcrm_order_class->cmbird_zcrm_order_sync( $order_id );
110 } catch ( \Throwable $e ) {
111 error_log( 'Error in cmbird_orders_prepare_sync: ' . $e->getMessage() ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Required for security logging.
112 }
113 }
114 }
115
116 /**
117 * Sync order when it's created via the WC API.
118 *
119 * @param WC_Data $object Inserted object.
120 * @param WP_REST_Request $request Request object.
121 * @param boolean $is_creating True when creating object, false when updating.
122 */
123 public function cmbird_on_insert_rest_api( $object, $request, $is_creating ) {
124 // $fd = fopen( __DIR__ . '/on_insert_rest_api.txt', 'w+' );
125 $request_body = $request->get_body();
126 $request_body_array = json_decode( $request_body, true );
127 // if the request body is empty or not an array, return early.
128 if ( empty( $request_body_array ) || ! is_array( $request_body_array ) ) {
129 return;
130 }
131 $order_status = $request_body_array['status'];
132 $order_id = $object->get_id();
133
134 if ( get_option( 'cmbird_zoho_inventory_access_token' ) ) {
135 $zi_order_class = new CMBIRD_Order_Sync_ZI();
136 // Check how many keys there are in the request body array. If there are only two keys then we don't need to do anything.
137 if ( count( $request_body_array ) === 2 && null !== $request_body_array ) {
138 if ( in_array( $order_status, array( 'cancelled', 'wc-merged' ) ) ) {
139 $zi_order_class->salesorder_void( $order_id );
140 }
141 } else {
142 $zi_order_class->zi_order_sync( $order_id );
143 }
144 }
145 // same for Zoho CRM.
146 if ( get_option( 'cmbird_zoho_crm_access_token' ) ) {
147 $zcrm_order_class = new CMBIRD_ZCRM_SalesOrder();
148 // Check how many keys there are in the request body array. If there are only two keys then we don't need to do anything.
149 if ( count( $request_body_array ) === 2 && null !== $request_body_array ) {
150 return;
151 } else {
152 $zcrm_order_class->cmbird_zcrm_order_sync( $order_id );
153 }
154 }
155
156 // fclose($fd);
157 }
158
159 /**
160 * Sync Renewal Order to Zoho once it's created.
161 *
162 * @param WC_Order $renewal_order The renewal order object.
163 * @param WC_Subscription $subscription The subscription object.
164 * @return WC_Order The renewal order object.
165 */
166 public function cmbird_zi_sync_renewal_order( $renewal_order, $subscription ) {
167 $order_id = $renewal_order->get_id();
168
169 // Sync the order to Zoho CRM.
170 if ( get_option( 'cmbird_zoho_crm_access_token' ) ) {
171 $zcrm_order_class = new CMBIRD_ZCRM_SalesOrder();
172 $zcrm_order_class->cmbird_zcrm_order_sync( $order_id );
173 }
174
175 // Sync the order to Zoho Inventory.
176 if ( get_option( 'cmbird_zoho_inventory_access_token' ) ) {
177 $zi_order_class = new CMBIRD_Order_Sync_ZI();
178 $zi_order_class->zi_order_sync( $order_id );
179 }
180
181 return $renewal_order;
182 }
183
184 /**
185 * Sync a WooCommerce order to Zoho CRM and Zoho Inventory.
186 *
187 * Called via AJAX from the Zoho Order Sync admin page.
188 *
189 * @param int $order_id The order ID to sync.
190 *
191 * @return void
192 */
193 public function cmbird_zoho_order_sync( $order_id ) {
194 if ( ! $order_id && isset( $_POST['nonce'], $_POST['arg_order_data'] ) ) {
195 if ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'zoho_admin_order_sync' ) ) {
196 wp_send_json_error( 'Nonce verification failed' );
197 }
198 $order_id = sanitize_text_field( wp_unslash( $_POST['arg_order_data'] ) );
199 }
200
201 if ( $order_id <= 0 ) {
202 wp_send_json_error( 'Invalid order ID.' );
203 }
204
205 // Sync the order to Zoho CRM.
206 if ( get_option( 'cmbird_zoho_crm_access_token' ) ) {
207 $zcrm_order_class = new CMBIRD_ZCRM_SalesOrder();
208 $zcrm_order_class->cmbird_zcrm_order_sync( $order_id );
209 }
210
211 // Sync the order to Zoho Inventory.
212 if ( get_option( 'cmbird_zoho_inventory_access_token' ) ) {
213 $zi_order_class = new CMBIRD_Order_Sync_ZI();
214 $zi_order_class->zi_order_sync( $order_id );
215 }
216
217 wp_send_json_success( 'Order synced successfully.' );
218 }
219
220 /**
221 * Function to clear all orphan data.
222 */
223 public function clear_orphan_data() {
224 global $wpdb;
225 // Delete orphaned product variations.
226 $deleted_variations = absint(
227 $wpdb->query(
228 "DELETE products
229 FROM {$wpdb->posts} products
230 LEFT JOIN {$wpdb->posts} wp ON wp.ID = products.post_parent
231 WHERE wp.ID IS NULL AND products.post_type = 'product_variation';"
232 )
233 );
234 // Delete orphaned postmeta.
235 $deleted_postmeta = absint(
236 $wpdb->query(
237 "DELETE pm
238 FROM {$wpdb->postmeta} pm
239 LEFT JOIN {$wpdb->posts} wp ON wp.ID = pm.post_id
240 WHERE wp.ID IS NULL;"
241 )
242 );
243 // Return the number of deleted entries (orphaned variations + orphaned postmeta).
244 return $deleted_variations + $deleted_postmeta;
245 }
246
247 /**
248 * Get the tax class based on the tax percentage.
249 *
250 * @param float $percentage The tax percentage.
251 * @return string|false The tax class if found, or standard if not found.
252 */
253 public function get_tax_class_by_percentage( $percentage ) {
254 // $fd = fopen( __DIR__ . '/get_tax_class_by_percentage.txt', 'a+' );
255
256 global $wpdb;
257 // Determine the number of decimal places in the provided percentage.
258 $decimal_places = strlen( substr( strrchr( $percentage, '.' ), 1 ) );
259
260 // Round the percentage to the determined number of decimal places.
261 $rounded_percentage = round( $percentage, $decimal_places );
262
263 // Try to get from cache first.
264 $cache_key = 'cmbird_tax_rate_' . md5( $rounded_percentage . '_' . $decimal_places );
265 $tax_rates = wp_cache_get( $cache_key, 'commercebird' );
266
267 if ( false === $tax_rates ) {
268 $tax_rates = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}woocommerce_tax_rates WHERE ROUND(tax_rate, %d) = %f", $decimal_places, $rounded_percentage ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Cached above.
269 wp_cache_set( $cache_key, $tax_rates, 'commercebird', HOUR_IN_SECONDS );
270 }
271
272 // If tax rates are found.
273 if ( $tax_rates ) {
274 // Get the tax class from the first matching tax rate.
275 $tax_class = $tax_rates[0]->tax_rate_class;
276 return $tax_class;
277 } else {
278 // Return null if no tax rates match the provided percentage.
279 return 'standard';
280 }
281 }
282
283 /**
284 * Get the WooCommerce tax class for a given Zoho tax ID using the configured mapping.
285 *
286 * @param string $zoho_tax_id The Zoho Inventory tax ID.
287 * @return string|null The WooCommerce tax_rate_class, or null if no mapping found.
288 */
289 public function get_tax_class_by_zoho_id( $zoho_tax_id ) {
290 if ( empty( $zoho_tax_id ) ) {
291 return null;
292 }
293
294 $cache_key = 'cmbird_tax_class_zoho_' . md5( $zoho_tax_id );
295 $cached = wp_cache_get( $cache_key, 'commercebird' );
296
297 if ( false !== $cached ) {
298 return '' !== $cached ? $cached : null;
299 }
300
301 global $wpdb;
302
303 // Find all configured Zoho→WC tax mappings.
304 $mappings = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Cached above.
305 $wpdb->prepare(
306 "SELECT option_name, option_value FROM {$wpdb->options} WHERE option_name LIKE %s",
307 'cmbird_zoho_inventory_tax_rate_%'
308 )
309 );
310
311 $wc_tax_rate_id = null;
312 foreach ( $mappings as $mapping ) {
313 $parts = explode( '##', $mapping->option_value );
314 if ( isset( $parts[0] ) && $parts[0] === $zoho_tax_id ) {
315 $wc_tax_rate_id = substr( $mapping->option_name, strlen( 'cmbird_zoho_inventory_tax_rate_' ) );
316 break;
317 }
318 }
319
320 if ( null === $wc_tax_rate_id ) {
321 wp_cache_set( $cache_key, '', 'commercebird', HOUR_IN_SECONDS );
322 return null;
323 }
324
325 $rate = $wpdb->get_row( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Cached above.
326 $wpdb->prepare(
327 "SELECT tax_rate_class FROM {$wpdb->prefix}woocommerce_tax_rates WHERE tax_rate_id = %d",
328 $wc_tax_rate_id
329 )
330 );
331
332 $tax_class = $rate ? $rate->tax_rate_class : null;
333 wp_cache_set( $cache_key, $tax_class ?? '', 'commercebird', HOUR_IN_SECONDS );
334
335 return $tax_class;
336 }
337
338 /**
339 * Send email to admin.
340 *
341 * @param string $subject The subject of the email.
342 * @param string $message The message body of the email.
343 *
344 * @return void
345 */
346 public function send_email( $subject, $message ) {
347 $admin_email = get_option( 'admin_email' );
348 $headers = array( 'Content-Type: text/html; charset=UTF-8' );
349
350 // Send the email.
351 wp_mail( $admin_email, $subject, $message, $headers );
352 }
353 }
354 }
355 $cmbird_common_functions = new CMBIRD_Common_Functions();
356