PluginProbe ʕ •ᴥ•ʔ
CommerceBird – AI Command Center, ERP Integrations & B2B for WooCommerce (Zoho, Exact Online). / 2.7.5
CommerceBird – AI Command Center, ERP Integrations & B2B for WooCommerce (Zoho, Exact Online). v2.7.5
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 / apis / class-api-for-product-webhook.php
commercebird / includes / classes / apis Last commit date
class-api-for-cmbird.php 7 months ago class-api-for-exact-webhooks.php 5 months ago class-api-for-product-webhook.php 5 months ago class-api-for-shipping-status.php 9 months ago class-api-for-woo-order.php 4 months ago class-api-for-zoho-inventory.php 9 months ago class-commercebird-list-items-api-controller.php 10 months ago class-commercebird-media-api-controller.php 10 months ago class-commercebird-metadata-controller.php 5 months ago index.php 1 year ago trait-api-permission.php 7 months ago
class-api-for-product-webhook.php
700 lines
1 <?php
2
3 namespace CommerceBird\API;
4
5 if ( ! defined( 'ABSPATH' ) ) {
6 exit;
7 }
8
9 use CMBIRD_Image_ZI;
10 use CMBIRD_Products_ZI;
11 use CMBIRD_Products_ZI_Export;
12 use WC_Data_Exception;
13 use WC_Product_Variation;
14 use WC_Product_Variable;
15 use WP_REST_Response;
16 use wpdb;
17 use CMBIRD_Common_Functions;
18 use Automattic\WooCommerce\Internal\ProductAttributesLookup\LookupDataStore;
19
20 /**
21 * Handles product webhook operations for CommerceBird API.
22 */
23 class ProductWebhook {
24
25
26 use Api;
27
28 /**
29 * REST API endpoint for Zoho product webhook.
30 *
31 * @var string
32 */
33 private static string $endpoint = 'zoho-product';
34
35 /**
36 * Indicates if WooCommerce taxes are enabled.
37 *
38 * @var bool
39 */
40 private $is_tax_enabled;
41
42 /**
43 * ProductWebhook constructor.
44 * Registers REST route, checks tax settings, and sets up hooks for queue processing and cleanup.
45 */
46 public function __construct() {
47 register_rest_route(
48 self::$namespace,
49 self::$endpoint,
50 array(
51 'methods' => 'POST',
52 'callback' => array( $this, 'handle' ),
53 'permission_callback' => '__return_true',
54 )
55 );
56 // Check if WooCommerce taxes are enabled and store the result.
57 $this->is_tax_enabled = 'yes' === get_option( 'woocommerce_calc_taxes' );
58
59 // Register queue processing hook.
60 add_action( 'cmbird_process_webhook_queue', array( $this, 'process_queued_requests' ) );
61
62 // Add cleanup for old rate limit data.
63 add_action( 'wp_scheduled_delete', array( $this, 'cleanup_rate_limit_data' ) );
64 }
65
66 /**
67 * Retrieves whether WooCommerce taxes are enabled.
68 *
69 * @return bool
70 */
71 public function is_tax_enabled(): bool {
72 return $this->is_tax_enabled;
73 }
74
75
76 /**
77 * Processes incoming product webhook data.
78 *
79 * @throws WC_Data_Exception If there is an error with WooCommerce product data.
80 */
81 private function process( array $data ): WP_REST_Response {
82 $response = new WP_REST_Response();
83 $response->set_data( $this->empty_response );
84 $response->set_status( 404 );
85 if ( ! array_key_exists( 'item', $data ) && ! array_key_exists( 'inventory_adjustment', $data ) ) {
86 return $response;
87 }
88
89 // Accounting stock mode check.
90 $accounting_stock = get_option( 'cmbird_zoho_enable_accounting_stock_status' );
91 $zi_enable_locationstock = get_option( 'cmbird_zoho_enable_locationstock_status' );
92 $location_id = get_option( 'cmbird_zoho_location_id_status' );
93
94 // variable item sync.
95 if ( array_key_exists( 'item', $data ) ) {
96 return $this->process_product_data( $data['item'], $zi_enable_locationstock, $location_id, $accounting_stock );
97 }
98 // inventory_adjustment.
99 if ( array_key_exists( 'inventory_adjustment', $data ) ) {
100 return $this->inventory_adjustment( $data['inventory_adjustment'] );
101 }
102
103 return $response;
104 }
105
106 /**
107 * Processes incoming product webhook data.
108 *
109 * @param array $item - incoming product webhook data.
110 * @param bool $zi_enable_locationstock - location stock enabled status.
111 * @param int $location_id - location id.
112 * @param bool $accounting_stock - accounting stock status.
113 *
114 * @return WP_REST_Response
115 * @throws WC_Data_Exception If there is an error with WooCommerce product data.
116 */
117 public function process_product_data( $item, $zi_enable_locationstock, $location_id, $accounting_stock ): WP_REST_Response {
118 // $fd = fopen( __DIR__ . '/process_product_data.txt', 'a+' );
119
120 // clean orphaned data from the database.
121 $common_class = new CMBIRD_Common_Functions();
122 $common_class->clear_orphan_data();
123
124 global $wpdb;
125 $item_id = $item['item_id'];
126 $item_name = $item['name'];
127 $item_price = $item['rate'];
128 $item_sku = $item['sku'];
129 $item_description = $item['description'];
130 $item_status = $item['status'] === 'active' ? 'publish' : 'draft';
131 $item_brand = $item['brand'];
132 $category_id = $item['category_id'];
133 $custom_fields = $item['custom_fields'];
134 $item_image = $item['image_name'];
135 // Stock mode check.
136 $locations = $item['locations'] ?? $item['warehouses'] ?? array();
137 if ( $zi_enable_locationstock ) {
138 foreach ( $locations as $location ) {
139 // Check both location_id and warehouse_id fields against the configured location_id.
140 $location_match = ( isset( $location['location_id'] ) && $location['location_id'] === $location_id ) ||
141 ( isset( $location['warehouse_id'] ) && $location['warehouse_id'] === $location_id );
142
143 if ( $location_match ) {
144 if ( $accounting_stock ) {
145 // Check for warehouse_ prefix if warehouse_id was used, otherwise use location_ prefix!
146 $item_stock = isset( $location['warehouse_id'] )
147 ? $location['warehouse_available_for_sale_stock']
148 : $location['location_available_for_sale_stock'];
149 } else {
150 // Check for warehouse_ prefix if warehouse_id was used, otherwise use location_ prefix!
151 $item_stock = isset( $location['warehouse_id'] )
152 ? $location['warehouse_actual_available_for_sale_stock']
153 : $location['location_actual_available_for_sale_stock'];
154 }
155 }
156 }
157 } elseif ( $accounting_stock && ! $zi_enable_locationstock ) {
158 $item_stock = $item['available_for_sale_stock'];
159 } else {
160 $item_stock = $item['actual_available_for_sale_stock'];
161 }
162 if ( isset( $item['group_name'] ) ) {
163 $group_name = $item['group_name'];
164 } else {
165 $group_name = '';
166 }
167 $item_category = $item['category_name'];
168 if ( isset( $item['group_id'] ) ) {
169 $groupid = $item['group_id'];
170 } else {
171 $groupid = '';
172 }
173
174 // Item package details.
175 $details = $item['package_details'];
176 $weight = floatval( $details['weight'] );
177 $length = floatval( $details['length'] );
178 $width = floatval( $details['width'] );
179 $height = floatval( $details['height'] );
180
181 // fwrite($fd, PHP_EOL . '$groupid : ' . $groupid);
182 if ( ! empty( $groupid ) ) {
183 // fwrite($fd, PHP_EOL . 'Inside grouped items');
184 // find parent variable product.
185 $row = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}postmeta WHERE meta_key='zi_item_id' AND meta_value=%s", $groupid ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- $groupid is prepared.
186 $group_id = $row->post_id;
187
188 if ( ! empty( $group_id ) ) {
189 $existing_parent_product = wc_get_product( $group_id );
190 $zi_disable_itemdescription_sync = get_option( 'cmbird_zoho_disable_description_sync_status' );
191 if ( ! empty( $item_description ) && ! $zi_disable_itemdescription_sync ) {
192 // fwrite($fd, PHP_EOL . 'Item description update : ' . $item_description);
193 $existing_parent_product->set_short_description( $item_description );
194 }
195 // Update the name of the variable product if allowed.
196 $zi_disable_itemname_sync = get_option( 'cmbird_zoho_disable_name_sync_status' );
197 if ( ! $zi_disable_itemname_sync ) {
198 $existing_parent_product->set_name( $item['group_name'] );
199 $slug = sanitize_title( $item['group_name'] );
200 $existing_parent_product->set_slug( $slug );
201 }
202 // Brand update if taxonomy product_brand exists.
203 if ( ! empty( $item_brand ) && taxonomy_exists( 'product_brand' ) ) {
204 wp_set_object_terms( $groupid, $item_brand, 'product_brand' );
205 } elseif ( ! empty( $item_brand ) && taxonomy_exists( 'product_brand' ) ) {
206 wp_set_object_terms( $groupid, $item_brand, 'product_brand' );
207 }
208 // Update the custom fields if the custom fields are not empty.
209 $cmbird_product_zi = new CMBIRD_Products_ZI();
210 if ( ! empty( $custom_fields ) ) {
211 $cmbird_product_zi->sync_item_custom_fields( $custom_fields, $groupid );
212 }
213
214 // Create or Update the Attributes.
215 // turn $item into array.
216 $gp_arr = (array) $item;
217 $attr_created = $cmbird_product_zi->sync_attributes_of_group( $gp_arr, $group_id );
218 if ( ! empty( $group_id ) && $attr_created ) {
219 // Create the variations.
220 $cmbird_product_zi->import_variable_product_variations( $item, $group_id );
221 }
222
223 // set the product status of the variable parent product.
224 // $existing_parent_product->set_status( $item_status );
225 $existing_parent_product->save();
226 } else {
227 // Add in scheduler to create the Variable Product.
228 $last_synced_page = get_option( 'cmbird_group_item_sync_page_cat_id_' . $category_id );
229 if ( ! intval( $last_synced_page ) ) {
230 $last_synced_page = 1;
231 }
232 $data = array(
233 'page' => $last_synced_page,
234 'category' => $category_id,
235 );
236 $existing_schedule = as_has_scheduled_action( 'import_group_items_cron', $data );
237 // Schedule the action if it doesn't exist.
238 if ( ! $existing_schedule ) {
239 as_schedule_single_action( time(), 'import_group_items_cron', $data );
240 }
241 }
242
243 $row_item = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}postmeta WHERE meta_key='zi_item_id' AND meta_value=%s", $item_id ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- item id is sanitized.
244 $variation_id = $row_item->post_id;
245 if ( $variation_id ) {
246 // updating existing variations.
247 $variation = new WC_Product_Variation( $variation_id );
248 // Prices.
249 if ( ! empty( $item_price ) ) {
250 $variation->set_price( $item_price );
251 }
252 $variation->set_regular_price( $item_price );
253 // Stock.
254 $zi_disable_stock_sync = get_option( 'cmbird_zoho_disable_stock_sync_status' );
255 if ( ! empty( $item_stock ) && ! $zi_disable_stock_sync ) {
256 // fwrite($fd, PHP_EOL . 'Stock is here:'. $item_stock );
257 $variation->set_stock_quantity( $item_stock );
258 $variation->set_manage_stock( true );
259 // $variation->set_stock_status('');
260 } else {
261 // fwrite($fd, PHP_EOL . 'Available Stock : false');
262 $variation->set_manage_stock( false );
263 }
264 // featured image.
265 $zi_disable_itemimage_sync = get_option( 'cmbird_zoho_disable_image_sync_status' );
266 if ( ! empty( $item_image ) && ! $zi_disable_itemimage_sync ) {
267 // fwrite($fd, PHP_EOL . 'Sync Image' );
268 $image_class = new CMBIRD_Image_ZI();
269 $image_class->cmbird_zi_get_image( $item_id, $item_name, $item_image, $variation_id );
270 }
271 // Disable or enable the variation based on the item_status.
272 $variation->set_status( $item_status );
273 // Update Purchase price.
274 $variation->update_meta_data( '_cogs_total_value', $item['purchase_rate'] );
275
276 // Map taxes while syncing product from zoho.
277 if ( $item['tax_id'] && ! $this->is_tax_enabled() ) {
278 $zi_common_class = new CMBIRD_Common_Functions();
279 $woo_tax_class = $zi_common_class->get_tax_class_by_percentage( $item['tax_percentage'] );
280 $variation->set_tax_status( 'taxable' );
281 $variation->set_tax_class( $woo_tax_class );
282 }
283 // weight & dimensions.
284 $variation->set_weight( $weight );
285 $variation->set_length( $length );
286 $variation->set_width( $width );
287 $variation->set_height( $height );
288
289 $variation->save(); // Save the data.
290 } elseif ( 'publish' === $item_status ) {
291 $attribute_name11 = $item['attribute_option_name1'];
292 $attribute_name12 = $item['attribute_option_name2'];
293 $attribute_name13 = $item['attribute_option_name3'];
294 // Prepare the variation data.
295 $attribute_arr = array();
296 if ( ! empty( $attribute_name11 ) ) {
297 $sanitized_name1 = wc_sanitize_taxonomy_name( $item['attribute_name1'] );
298 $attribute_arr[ $sanitized_name1 ] = $attribute_name11;
299 }
300 if ( ! empty( $attribute_name12 ) ) {
301 $sanitized_name2 = wc_sanitize_taxonomy_name( $item['attribute_name2'] );
302 $attribute_arr[ $sanitized_name2 ] = $attribute_name12;
303 }
304 if ( ! empty( $attribute_name13 ) ) {
305 $sanitized_name3 = wc_sanitize_taxonomy_name( $item['attribute_name3'] );
306 $attribute_arr[ $sanitized_name3 ] = $attribute_name13;
307 }
308
309 // here actually create new variation because sku not found.
310 $zi_disable_stock_sync = get_option( 'cmbird_zoho_disable_stock_sync_status' );
311 $variation = new WC_Product_Variation();
312 $variation->set_parent_id( $group_id );
313 $variation->set_status( 'publish' );
314 $variation->set_regular_price( $item_price );
315 $variation->set_sku( $item_sku );
316 $variation->set_weight( $weight );
317 $variation->set_length( $length );
318 $variation->set_width( $width );
319 $variation->set_height( $height );
320 if ( ! $zi_disable_stock_sync ) {
321 $variation->set_stock_quantity( $item_stock );
322 $variation->set_manage_stock( true );
323 $variation->set_stock_status( '' );
324 } else {
325 $variation->set_manage_stock( false );
326 }
327 // Map taxes while syncing product from zoho.
328 if ( $item['tax_id'] && ! $this->is_tax_enabled() ) {
329 $zi_common_class = new CMBIRD_Common_Functions();
330 $woo_tax_class = $zi_common_class->get_tax_class_by_percentage( $item['tax_percentage'] );
331 $variation->set_tax_status( 'taxable' );
332 $variation->set_tax_class( $woo_tax_class );
333 }
334 $variation->add_meta_data( 'zi_item_id', $item->item_id );
335 $variation_id = $variation->save();
336
337 // Get the variation attributes with correct attribute values.
338 foreach ( $attribute_arr as $attribute => $term_name ) {
339 $taxonomy = $attribute;
340 // If taxonomy doesn't exists we create it.
341 if ( ! taxonomy_exists( $taxonomy ) ) {
342 register_taxonomy(
343 $taxonomy,
344 'product_variation',
345 array(
346 'hierarchical' => false,
347 'label' => ucfirst( $attribute ),
348 'query_var' => true,
349 'rewrite' => array( 'slug' => sanitize_title( $attribute ) ),
350 ),
351 );
352 }
353
354 // Check if the Term name exist and if not we create it.
355 if ( ! term_exists( $term_name, $taxonomy ) ) {
356 wp_insert_term( $term_name, $taxonomy );
357 }
358
359 $term_slug = get_term_by( 'name', $term_name, $taxonomy )->slug;
360 // Get the post Terms names from the parent variable product.
361 $post_term_names = wp_get_post_terms( $group_id, $taxonomy, array( 'fields' => 'names' ) );
362 // Check if the post term exist and if not we set it in the parent variable product.
363 if ( ! in_array( $term_name, $post_term_names, true ) ) {
364 wp_set_post_terms( $group_id, $term_name, $taxonomy, true );
365 }
366 // Set/save the attribute data in the product variation.
367 update_post_meta( $variation_id, 'attribute_' . $taxonomy, $term_slug );
368 }
369
370 // featured image.
371 $zi_disable_itemimage_sync = get_option( 'cmbird_zoho_disable_image_sync_status' );
372 if ( ! empty( $item_image ) && ! $zi_disable_itemimage_sync ) {
373 $image_class = new CMBIRD_Image_ZI();
374 $image_class->cmbird_zi_get_image( $item_id, $item_name, $item_image, $variation_id );
375 }
376
377 update_post_meta( $variation_id, 'zi_item_id', $item_id );
378 // Sync variation data with parent variable product.
379 WC_Product_Variable::sync( $group_id );
380 // Regenerate lookup table for attributes.
381 $lookup_data_store = new LookupDataStore();
382 $lookup_data_store->create_data_for_product( $group_id );
383 // End group item add process.
384 unset( $attribute_arr );
385 }
386 wc_delete_product_transients( $group_id ); // Clear/refresh cache.
387 // end of grouped item creation.
388 } else {
389 // fwrite($fd, PHP_EOL . 'Inside simple items');
390 // fwrite($fd, PHP_EOL . 'Item description Simple : ' . $item_description);
391 $row_item = $wpdb->get_row(
392 $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}postmeta WHERE meta_key = 'zi_item_id' AND meta_value = %s", $item_id )
393 ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared // item id is sanitized.
394 $mapped_product_id = $row_item->post_id;
395 // simple product.
396 // fwrite($fd, PHP_EOL . 'Before Match check');
397 $pdt_id = '';
398 if ( ! empty( $mapped_product_id ) && null !== $mapped_product_id ) {
399 $product_found = wc_get_product( $mapped_product_id );
400 if ( ! $product_found ) {
401 // remove all postmeta of that product id.
402 $wpdb->delete( $wpdb->postmeta, array( 'post_id' => $mapped_product_id ) );
403 } else {
404 $pdt_id = $mapped_product_id;
405 }
406 } elseif ( empty( $item['is_combo_product'] ) ) {
407 // fwrite($fd, PHP_EOL . 'Inside create product');
408
409 // Check if Category is selected before creating simple item.
410 if ( 'publish' === $item_status ) {
411 $opt_category = get_option( 'cmbird_zoho_item_category' );
412 $opt_category = maybe_unserialize( $opt_category );
413 $category_id = $item['category_id'];
414 if ( $opt_category ) {
415 if ( in_array( $category_id, $opt_category, true ) ) {
416 $product_class = new CMBIRD_Products_ZI_Export();
417 $pdt_id = $product_class->cmbird_zi_product_to_woocommerce( $item, $item_stock );
418 }
419 }
420 }
421 // fwrite($fd, PHP_EOL . 'After adding it : ' . $pdt_id);
422 }
423
424 // If there is product id then update metadata.
425 if ( ! empty( $pdt_id ) ) {
426 $simple_product = wc_get_product( $pdt_id );
427 // update the name if its allowed.
428 $zi_disable_itemname_sync = get_option( 'cmbird_zoho_disable_name_sync_status' );
429 if ( ! $zi_disable_itemname_sync ) {
430 $simple_product->set_name( $item_name );
431 $slug = sanitize_title( $item_name );
432 $simple_product->set_slug( $slug );
433 }
434 // update the zi_item_id using the product instance.
435 $simple_product->update_meta_data( 'zi_item_id', $item_id );
436 // update the status using set_status().
437 $simple_product->set_status( $item_status );
438 // Update the product SKU.
439 $simple_product->set_sku( $item_sku );
440 // price.
441 $sale_price = $simple_product->get_sale_price();
442 $simple_product->set_regular_price( $item_price );
443 if ( empty( $sale_price ) ) {
444 $simple_product->set_price( $item_price );
445 }
446 // Update Purchase price.
447 $simple_product->update_meta_data( key: '_cogs_total_value', value: $item['purchase_rate'] );
448 // description.
449 $zi_disable_itemdescription_sync = get_option( 'cmbird_zoho_disable_description_sync_status' );
450 if ( ! empty( $item_description ) && ! $zi_disable_itemdescription_sync ) {
451 $simple_product->set_short_description( $item_description );
452 }
453 // Brand update if taxonomy product_brand(s) exists.
454 if ( ! empty( $item_brand ) && taxonomy_exists( 'product_brand' ) ) {
455 wp_set_object_terms( $pdt_id, $item_brand, 'product_brand' );
456 } elseif ( ! empty( $item_brand ) && taxonomy_exists( 'product_brand' ) ) {
457 wp_set_object_terms( $pdt_id, $item_brand, 'product_brand' );
458 }
459 // stock.
460 $zi_disable_stock_sync = get_option( 'cmbird_zoho_disable_stock_sync_status' );
461 if ( ! $zi_disable_stock_sync ) {
462 // fwrite( $fd, PHP_EOL . 'Inside1' );
463 if ( 'NULL' !== gettype( $item_stock ) ) {
464 // fwrite( $fd, PHP_EOL . 'Inside1.1' );
465 // Set manage stock to yes.
466 $simple_product->set_manage_stock( true );
467 // Update stock for simple product.
468 $simple_product->set_stock_quantity( number_format( $item_stock, 0, '.', '' ) );
469 if ( $item_stock > 0 ) {
470 // fwrite( $fd, PHP_EOL . 'Inside2' );
471 // Update stock status.
472 $simple_product->set_stock_status( 'instock' );
473 wp_set_post_terms( $pdt_id, 'instock', 'product_visibility', true );
474 } else {
475 // fwrite($fd, PHP_EOL . 'Inside3');
476 $stock_status = $simple_product->backorders_allowed() ? 'onbackorder' : 'outofstock';
477 $simple_product->set_stock_status( $stock_status );
478 wp_set_post_terms( $pdt_id, $stock_status, 'product_visibility', true );
479 }
480 }
481 }
482 // fwrite($fd, PHP_EOL . 'After stock');
483 // Update weight & dimensions of simple product.
484 $simple_product->set_weight( $weight );
485 $simple_product->set_length( $length );
486 $simple_product->set_width( $width );
487 $simple_product->set_height( $height );
488
489 // featured image.
490 $zi_disable_itemimage_sync = get_option( 'cmbird_zoho_disable_image_sync_status' );
491 if ( ! empty( $item_image ) && ! $zi_disable_itemimage_sync ) {
492 $image_class = new CMBIRD_Image_ZI();
493 $image_class->cmbird_zi_get_image( $item_id, $item_name, $item_image, $pdt_id );
494 }
495
496 // category.
497 if ( ! empty( $item_category ) && empty( $group_name ) ) {
498 $term = get_term_by( 'name', $item_category, 'product_cat' );
499 $term_id = $term->term_id;
500 if ( empty( $term_id ) ) {
501 $term = wp_insert_term(
502 $item_category,
503 'product_cat',
504 array(
505 'parent' => 0,
506 )
507 );
508 $term_id = $term->term_id;
509 }
510 // Remove "uncategorized" category if assigned.
511 $uncategorized_term = get_term_by( 'slug', 'uncategorized', 'product_cat' );
512 if ( $uncategorized_term && has_term( $uncategorized_term->term_id, 'product_cat', $pdt_id ) ) {
513 wp_remove_object_terms( $pdt_id, $uncategorized_term->term_id, 'product_cat' );
514 }
515 if ( ! is_wp_error( $term_id ) && isset( $term->term_id ) ) {
516 $existing_terms = wp_get_object_terms( $pdt_id, 'product_cat' );
517 if ( $existing_terms && count( $existing_terms ) > 0 ) {
518 $import_class = new CMBIRD_Products_ZI();
519 $is_term_exist = $import_class->zi_check_terms_exists( $existing_terms, $term_id );
520 if ( ! $is_term_exist ) {
521 $simple_product->update_meta_data( 'zi_category_id', $item['category_id'] );
522 wp_add_object_terms( $pdt_id, $term_id, 'product_cat' );
523 }
524 } else {
525 $simple_product->update_meta_data( 'zi_category_id', $item['category_id'] );
526 wp_set_object_terms( $pdt_id, $term_id, 'product_cat' );
527 }
528 }
529 }
530
531 // Update the custom fields if the custom fields are not empty.
532 if ( ! empty( $custom_fields ) ) {
533 $import_class = new CMBIRD_Products_ZI();
534 $import_class->sync_item_custom_fields( $custom_fields, $pdt_id );
535 }
536
537 // Map taxes while syncing product from zoho.
538 if ( $item['tax_id'] && ! $this->is_tax_enabled() ) {
539 $zi_common_class = new CMBIRD_Common_Functions();
540 $woo_tax_class = $zi_common_class->get_tax_class_by_percentage( $item['tax_percentage'] );
541 $simple_product->set_tax_status( 'taxable' );
542 $simple_product->set_tax_class( $woo_tax_class );
543 }
544 $simple_product->save();
545 wc_delete_product_transients( $pdt_id );
546 }
547 }
548 $response = new WP_REST_Response();
549 $response->set_data( 'success on variable product' );
550 $response->set_status( 200 );
551
552 // fclose( $fd ); // close logfile.
553
554 return $response;
555 }
556
557 /**
558 * Process queued webhook requests
559 * Called by WordPress cron system when queue processing is scheduled
560 */
561 public function process_queued_requests() {
562 $queue_key = 'cmbird_request_queue';
563 $current_queue = get_option( $queue_key, array() );
564
565 if ( empty( $current_queue ) || ! is_array( $current_queue ) ) {
566 return;
567 }
568
569 $current_time = time();
570 $processed_count = 0;
571 $max_process_per_batch = 10; // Process up to 10 requests per batch.
572
573 // Process queue items.
574 foreach ( $current_queue as $index => $queue_item ) {
575 if ( $processed_count >= $max_process_per_batch ) {
576 break; // Don't process too many at once.
577 }
578
579 // Check if request is too old (older than 5 minutes).
580 if ( ( $current_time - $queue_item['timestamp'] ) > 300 ) {
581 unset( $current_queue[ $index ] );
582 continue;
583 }
584
585 // Simulate the original request processing.
586 try {
587 if ( isset( $queue_item['data'] ) && ! empty( $queue_item['data'] ) ) {
588 $data = $queue_item['data'];
589
590 // Convert JSONString if present.
591 if ( array_key_exists( 'JSONString', $data ) ) {
592 $data = str_replace( '\\', '', $data['JSONString'] );
593 }
594
595 // Process the data.
596 $this->process( $data );
597 ++$processed_count;
598 }
599 } catch ( Exception $e ) {
600 // Log error but continue processing other items.
601 error_log( 'CommerceBird Webhook Queue Processing Error: ' . $e->getMessage() ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Required for security logging.
602 }
603
604 // Remove processed item from queue.
605 unset( $current_queue[ $index ] );
606 }
607
608 // Re-index array after unsetting elements.
609 $current_queue = array_values( $current_queue );
610
611 // Update the queue.
612 update_option( $queue_key, $current_queue );
613
614 // Schedule next processing if queue still has items.
615 if ( ! empty( $current_queue ) && ! wp_next_scheduled( 'cmbird_process_webhook_queue' ) ) {
616 wp_schedule_single_event( $current_time + 30, 'cmbird_process_webhook_queue' );
617 }
618 }
619
620 /**
621 * Cleanup old rate limiting data
622 * Called by WordPress scheduled delete action
623 */
624 public function cleanup_rate_limit_data() {
625 global $wpdb;
626
627 // Clean up old transients related to rate limiting.
628 $wpdb->query(
629 $wpdb->prepare(
630 "DELETE FROM {$wpdb->options}
631 WHERE option_name LIKE %s
632 OR option_name LIKE %s",
633 $wpdb->esc_like( '_transient_cmbird_rate_limit_' ) . '%',
634 $wpdb->esc_like( '_transient_timeout_cmbird_rate_limit_' ) . '%'
635 )
636 ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- safe here.
637
638 // Clean up old processing locks (older than 1 hour).
639 $wpdb->query(
640 $wpdb->prepare(
641 "DELETE FROM {$wpdb->options}
642 WHERE option_name LIKE %s
643 AND option_value < %d",
644 $wpdb->esc_like( 'cmbird_processing_payload_' ) . '%',
645 time() - 3600
646 )
647 ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- time() is safe here.
648 }
649
650 /**
651 * Handles inventory adjustment webhook data.
652 *
653 * @param array $inventory_adjustment Inventory adjustment data.
654 *
655 * @return WP_REST_Response
656 */
657 public function inventory_adjustment( $inventory_adjustment ): WP_REST_Response {
658 global $wpdb;
659 $item = $inventory_adjustment;
660 $line_items = $item['line_items'];
661 // get first item from line items array.
662 $item_id = $line_items[0]['item_id'];
663 $adjusted_stock = $line_items[0]['quantity_adjusted'];
664
665 $row_item = $wpdb->get_row(
666 $wpdb->prepare(
667 "SELECT * FROM {$wpdb->prefix}postmeta WHERE meta_key = 'zi_item_id' AND meta_value = %s",
668 $item_id
669 )
670 ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $item_id is prepared.
671 $mapped_product_id = $row_item->post_id;
672
673 if ( ! empty( $mapped_product_id ) ) {
674 // stock.
675 $zi_disable_stock_sync = get_option( 'cmbird_zoho_disable_stock_sync_status' );
676 $product = wc_get_product( $mapped_product_id );
677 // Check if the product is in stock.
678 if ( ! $zi_disable_stock_sync ) {
679 if ( $product->is_in_stock() ) {
680 // Get stock quantity.
681 $stock_quantity = $product->get_stock_quantity();
682 $new_stock = $stock_quantity + $adjusted_stock;
683 $product->set_stock_quantity( $new_stock );
684 } else {
685 $product->set_stock_quantity( $adjusted_stock );
686 $product->set_stock_status( 'instock' );
687 $product->set_manage_stock( true );
688 }
689 $product->save();
690 }
691 }
692
693 $response = new WP_REST_Response();
694 $response->set_data( 'Inventory Adjustment successful' );
695 $response->set_status( 200 );
696
697 return $response;
698 }
699 }
700