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