PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 1.1.4
MxChat – AI Chatbot & Content Generation for WordPress v1.1.4
3.2.21 3.2.20 3.2.19 3.2.18 3.2.17 3.2.16 3.2.15 3.2.14 3.2.12 3.2.13 3.2.11 3.2.10 3.2.9 3.2.8 3.2.7 3.2.6 3.2.5 3.2.4 3.2.3 3.2.2 3.2.1 2.0.3 2.0.4 2.0.5 2.0.6 All 152 releases
mxchat-basic / includes / class-mxchat-integrator.php

class-mxchat-integrator.php in MxChat – AI Chatbot & Content Generation for WordPress 1.1.4, at includes/class-mxchat-integrator.php

652 lines 23.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 if (!defined('ABSPATH')) {
3 exit;
4 }
5
6 class MxChat_Integrator {
7 private $options;
8 private $chat_count;
9
10 public function __construct() {
11 $this->options = get_option('mxchat_options');
12 $this->chat_count = get_option('mxchat_chat_count', 0);
13
14 // Add WooCommerce hooks
15 add_action('wp_insert_post', array($this, 'mxchat_handle_product_change'), 10, 3);
16
17 // Ensure embeddings are removed when a product is moved to trash or permanently deleted
18 add_action('wp_trash_post', array($this, 'mxchat_handle_product_delete'));
19 add_action('before_delete_post', array($this, 'mxchat_handle_product_delete'));
20
21 add_action('wp_enqueue_scripts', array($this, 'mxchat_enqueue_scripts_styles'));
22 add_action('wp_ajax_mxchat_handle_chat_request', array($this, 'mxchat_handle_chat_request'));
23 add_action('wp_ajax_nopriv_mxchat_handle_chat_request', array($this, 'mxchat_handle_chat_request'));
24 add_action('wp_ajax_mxchat_dismiss_pre_chat_message', array($this, 'mxchat_dismiss_pre_chat_message'));
25 add_action('wp_ajax_nopriv_mxchat_dismiss_pre_chat_message', array($this, 'mxchat_dismiss_pre_chat_message'));
26
27 if (!wp_next_scheduled('mxchat_reset_rate_limits')) {
28 wp_schedule_event(time(), 'daily', 'mxchat_reset_rate_limits');
29 }
30
31 add_action('mxchat_reset_rate_limits', array($this, 'mxchat_reset_rate_limits'));
32 }
33
34 public function mxchat_handle_product_change($post_id, $post, $update) {
35 // Ensure this is a product post type
36 if ($post->post_type !== 'product') {
37 return;
38 }
39
40 // Only generate embeddings if the product is published
41 if ($post->post_status === 'publish') {
42 // Delay the embedding slightly to ensure all product data is available
43 add_action('shutdown', function() use ($post_id) {
44 $product = wc_get_product($post_id);
45 if ($product && $product->get_price() !== '') {
46 $this->mxchat_store_product_embedding($product);
47 } else {
48 // Optionally, log or handle the case where product data is incomplete
49 error_log("Product {$post_id} does not have complete data. Embedding not generated.");
50 }
51 });
52 }
53 }
54
55 public function mxchat_handle_product_delete($post_id) {
56 if (get_post_type($post_id) !== 'product') {
57 return;
58 }
59
60 global $wpdb;
61 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
62
63 // Delete the embedding associated with this product
64 $wpdb->delete($table_name, array('source_url' => get_permalink($post_id)), array('%s'));
65 }
66
67 private function mxchat_store_product_embedding($product) {
68 if (isset($this->options['enable_woocommerce_integration']) && $this->options['enable_woocommerce_integration'] === '1') {
69
70 $source_url = get_permalink($product->get_id());
71 $regular_price = $product->get_regular_price();
72 $sale_price = $product->get_sale_price();
73 $price = $sale_price ?: $regular_price;
74
75 $description = $product->get_description() . "\n\n" .
76 "Short Description: " . $product->get_short_description() . "\n" .
77 "Price: " . $regular_price . "\n" .
78 "Sale Price: " . ($sale_price ?: 'N/A') . "\n" .
79 "SKU: " . $product->get_sku();
80
81 global $wpdb;
82 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
83
84 // Delete any existing embedding for this product
85 $wpdb->delete($table_name, array('source_url' => $source_url), array('%s'));
86
87 // Submit the new content and embedding to the database
88 MxChat_Utils::submit_content_to_db($description, $source_url, $this->options['api_key']);
89 }
90 }
91
92
93
94
95
96 private function mxchat_increment_chat_count() {
97 $chat_count = get_option('mxchat_chat_count', 0);
98 $chat_count++;
99 update_option('mxchat_chat_count', $chat_count);
100 }
101
102 public function mxchat_fetch_conversation_history_for_ajax($session_id) {
103 global $wpdb;
104 $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
105
106 // Prepare and execute the query safely
107 $chat_transcripts = $wpdb->get_results(
108 $wpdb->prepare("SELECT * FROM $table_name WHERE session_id = %s ORDER BY timestamp ASC", sanitize_text_field($session_id))
109 );
110
111 // Check if results are empty
112 if (empty($chat_transcripts)) {
113 return [];
114 }
115
116 // Build the conversation history
117 $conversation_history = [];
118 foreach ($chat_transcripts as $transcript) {
119 $conversation_history[] = [
120 'role' => $transcript->role,
121 'content' => $transcript->message
122 ];
123 }
124
125 return $conversation_history;
126 }
127
128
129 private function mxchat_save_chat_message($session_id, $role, $message) {
130 global $wpdb;
131 $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
132
133 $user_id = is_user_logged_in() ? get_current_user_id() : 0;
134 $user_identifier = MxChat_User::mxchat_get_user_identifier();
135 $user_email = MxChat_User::mxchat_get_user_email();
136
137 $wpdb->insert($table_name, [
138 'user_id' => $user_id,
139 'user_identifier' => $user_identifier,
140 'user_email' => $user_email,
141 'session_id' => $session_id,
142 'role' => $role,
143 'message' => $message,
144 'timestamp' => current_time('mysql', 1)
145 ]);
146 }
147
148 public function mxchat_handle_chat_request() {
149 global $wpdb;
150
151 // Get and sanitize the user identifier
152 $user_id = $this->mxchat_get_user_identifier();
153 $user_id = sanitize_key($user_id);
154
155 // Setup rate limiting
156 $rate_limit_transient_key = 'mxchat_chat_limit_' . $user_id;
157 $chat_count = get_transient($rate_limit_transient_key) ?: 0;
158 $session_transient_key = 'mxchat_chat_session_' . $user_id;
159 $session_id = get_transient($session_transient_key) ?: uniqid('mxchat_chat_', true);
160
161 set_transient($session_transient_key, $session_id, DAY_IN_SECONDS);
162
163 // Check rate limit
164 $rate_limit_option = $this->options['rate_limit'] ?? 'unlimited';
165 if ($rate_limit_option !== 'unlimited' && $chat_count >= intval($rate_limit_option)) {
166 wp_send_json_error(array('message' => $this->options['rate_limit_message'] ?? 'Rate limit exceeded. Please try again later.'));
167 wp_die();
168 }
169 set_transient($rate_limit_transient_key, $chat_count + 1, DAY_IN_SECONDS);
170
171 // Validate and sanitize the incoming message
172 if (empty($_POST['message'])) {
173 wp_send_json_error('No message received');
174 wp_die();
175 }
176
177 $message = sanitize_text_field($_POST['message']);
178 $this->mxchat_save_chat_message($session_id, 'user', $message);
179
180 // Track email capture and WooCommerce flows with individual transients
181 $email_capture_prompt = get_transient('mxchat_email_capture_' . $user_id);
182 $interaction_count = get_transient('mxchat_email_interaction_count_' . $user_id) ?: 0;
183 $woocommerce_prompt = get_transient('mxchat_woocommerce_prompt_' . $user_id);
184
185 // Handle email capture flow
186 if ($email_capture_prompt) {
187 if (preg_match('/[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}/i', $message, $matches)) {
188 $email = $matches[0];
189
190 $this->add_email_to_loops($email);
191 $response = $this->options['email_capture_response'] ?? 'Thank you for providing your email! You\'ve been added to our list.';
192
193 delete_transient('mxchat_email_capture_' . $user_id);
194 delete_transient('mxchat_email_interaction_count_' . $user_id);
195 delete_transient('mxchat_woocommerce_prompt_' . $user_id);
196
197 $this->mxchat_save_chat_message($session_id, 'bot', $response);
198 wp_send_json(['message' => $response]);
199 wp_die();
200 } else {
201 if ($interaction_count >= 3) {
202 delete_transient('mxchat_email_capture_' . $user_id);
203 delete_transient('mxchat_email_interaction_count_' . $user_id);
204 // Allow the conversation to continue normally without forcing email capture
205 } else {
206 set_transient('mxchat_email_interaction_count_' . $user_id, ++$interaction_count, 5 * MINUTE_IN_SECONDS);
207 }
208 }
209 }
210
211 // Handle WooCommerce add-to-cart flow
212 if (stripos($message, 'add to cart') !== false) {
213 $last_product_id = get_transient('mxchat_last_discussed_product_' . $user_id);
214
215 if ($last_product_id) {
216 $added = WC()->cart->add_to_cart($last_product_id);
217 $product = wc_get_product($last_product_id);
218
219 if ($added) {
220 $response = "The product '{$product->get_name()}' has been added to your cart. To proceed to checkout, please type 'checkout'.";
221 set_transient('mxchat_checkout_prompt_' . $user_id, true, 5 * MINUTE_IN_SECONDS);
222
223 delete_transient('mxchat_email_capture_' . $user_id); // Clear email capture state to switch to cart flow
224 $this->mxchat_save_chat_message($session_id, 'bot', $response);
225 wp_send_json(['message' => $response]);
226 wp_die();
227 } else {
228 $response = "Sorry, I couldn't add the product to your cart. Please try again.";
229 $this->mxchat_save_chat_message($session_id, 'bot', $response);
230 wp_send_json(['message' => $response]);
231 wp_die();
232 }
233 } else {
234 $response = "I couldn't find the product to add. Please mention the product name again.";
235 $this->mxchat_save_chat_message($session_id, 'bot', $response);
236 wp_send_json(['message' => $response]);
237 wp_die();
238 }
239 }
240
241 // Handle checkout response
242 if (stripos($message, 'checkout') !== false) {
243 $checkout_prompt = get_transient('mxchat_checkout_prompt_' . $user_id);
244 if ($checkout_prompt && WC()->cart->get_cart_contents_count() > 0) {
245 $checkout_url = wc_get_checkout_url();
246 $response = "Great! Redirecting you to the checkout page...";
247 delete_transient('mxchat_checkout_prompt_' . $user_id);
248
249 $this->mxchat_save_chat_message($session_id, 'bot', $response);
250 wp_send_json(['message' => $response, 'redirect_url' => $checkout_url]);
251 wp_die();
252 } else {
253 $response = "It seems like there is no active checkout prompt or no items in your cart. Please add a product to the cart first.";
254 $this->mxchat_save_chat_message($session_id, 'bot', $response);
255 wp_send_json(['message' => $response]);
256 wp_die();
257 }
258 }
259
260 // Check for trigger keywords to initiate email capture
261 $trigger_keywords = explode(',', $this->options['trigger_keywords'] ?? '');
262 foreach ($trigger_keywords as $keyword) {
263 if (stripos($message, trim($keyword)) !== false) {
264 $response = $this->options['triggered_phrase_response'] ?? "Would you like to join our mailing list? Please provide your email below.";
265 set_transient('mxchat_email_capture_' . $user_id, true, 5 * MINUTE_IN_SECONDS);
266
267 $this->mxchat_save_chat_message($session_id, 'bot', $response);
268 wp_send_json(['message' => $response]);
269 wp_die();
270 }
271 }
272
273 // Store product discussion in transient
274 $last_discussed_product_id = MxChat_WooCommerce::mxchat_extract_product_id_from_message($message);
275 if ($last_discussed_product_id) {
276 set_transient('mxchat_last_discussed_product_' . $user_id, $last_discussed_product_id, 3600); // Cache for 1 hour
277 }
278
279 // Standard chat processing
280 $user_message_embedding = $this->mxchat_generate_embedding($message, $this->options['api_key']);
281 if (!is_array($user_message_embedding)) {
282 wp_send_json_error('Error processing your message.');
283 wp_die();
284 }
285
286 $relevant_content = $this->mxchat_find_relevant_content($user_message_embedding);
287 $conversation_history = $this->mxchat_fetch_conversation_history_for_ajax($session_id);
288 $this->mxchat_increment_chat_count();
289 $response = $this->mxchat_generate_response($relevant_content, $this->options['api_key'], $conversation_history);
290
291 $this->mxchat_save_chat_message($session_id, 'bot', $response);
292 wp_send_json(['message' => $response]);
293 wp_die();
294 }
295
296
297 // Function to add the captured email to Loops
298 private function add_email_to_loops($email) {
299 $api_key = $this->options['loops_api_key'];
300 $mailing_list_id = $this->options['loops_mailing_list'];
301
302 $data = array(
303 'email' => $email,
304 'subscribed' => true,
305 'source' => 'MxChat AI Chatbot',
306 'mailingLists' => array($mailing_list_id => true),
307 );
308
309 $url = "https://app.loops.so/api/v1/contacts/create";
310 $args = array(
311 'body' => json_encode($data),
312 'headers' => array(
313 'Authorization' => 'Bearer ' . $api_key,
314 'Content-Type' => 'application/json',
315 ),
316 'method' => 'POST',
317 'timeout' => 45,
318 );
319
320 wp_remote_post($url, $args);
321 }
322
323
324 private function mxchat_get_user_identifier() {
325 return MxChat_User::mxchat_get_user_identifier();
326 }
327
328
329
330 private function mxchat_generate_embedding($text, $api_key) {
331 $endpoint = 'https://api.openai.com/v1/embeddings';
332
333 $body = wp_json_encode([
334 'input' => $text,
335 'model' => 'text-embedding-ada-002'
336 ]);
337
338 $args = [
339 'body' => $body,
340 'headers' => [
341 'Content-Type' => 'application/json',
342 'Authorization' => 'Bearer ' . $api_key,
343 ],
344 'timeout' => 60,
345 'redirection' => 5,
346 'blocking' => true,
347 'httpversion' => '1.0',
348 'sslverify' => true,
349 ];
350
351 $response = wp_remote_post($endpoint, $args);
352
353 if (is_wp_error($response)) {
354 return null;
355 }
356
357 $response_body = json_decode(wp_remote_retrieve_body($response), true);
358
359 if (isset($response_body['data'][0]['embedding']) && is_array($response_body['data'][0]['embedding'])) {
360 return $response_body['data'][0]['embedding'];
361 } else {
362 return null;
363 }
364 }
365
366 private function mxchat_find_relevant_content($user_embedding) {
367 global $wpdb;
368 $system_prompt_table = $wpdb->prefix . 'mxchat_system_prompt_content';
369
370 // Define a cache key for embeddings
371 $cache_key = 'mxchat_system_prompt_embeddings';
372
373 // Attempt to get the embeddings from the cache
374 $embeddings = wp_cache_get($cache_key, 'mxchat_system_prompts');
375
376 if ($embeddings === false) {
377 // Cache miss, query the database and cache the results
378 $query = "SELECT id, embedding_vector FROM {$system_prompt_table}";
379 $embeddings = $wpdb->get_results($query);
380
381 if ($embeddings === null || empty($embeddings)) {
382 error_log("No embeddings found in the database.");
383 return null; // Return null to handle no embeddings gracefully
384 }
385
386 // Cache the results if successful
387 wp_cache_set($cache_key, $embeddings, 'mxchat_system_prompts', 3600); // Cache for 1 hour
388 }
389
390 $most_relevant_id = null;
391 $highest_similarity = -INF;
392
393 foreach ($embeddings as $embedding) {
394 $database_embedding = maybe_unserialize($embedding->embedding_vector);
395
396 // Debugging: Log the embeddings
397 // if (!is_array($database_embedding)) {
398 // error_log("Invalid database embedding format for ID {$embedding->id}: " . print_r($database_embedding, true));
399 // continue;
400 // }
401
402 if (is_array($user_embedding)) {
403 $similarity = $this->mxchat_calculate_cosine_similarity($user_embedding, $database_embedding);
404
405 // Debugging: Log the similarity score
406 // error_log("Calculated similarity for ID {$embedding->id}: {$similarity}");
407
408 if ($similarity > $highest_similarity) {
409 $highest_similarity = $similarity;
410 $most_relevant_id = $embedding->id;
411 }
412 } else {
413 // error_log("User embedding is not an array. Embedding data: " . print_r($user_embedding, true));
414 }
415 }
416
417 if ($most_relevant_id !== null) {
418 // Fetch content with product links
419 return $this->fetch_content_with_product_links($most_relevant_id);
420 }
421
422 error_log("No relevant content found. Most relevant ID was null.");
423 return null; // Return null if no relevant content is found
424 }
425
426
427 private function fetch_content_with_product_links($most_relevant_id) {
428 global $wpdb;
429 $system_prompt_table = $wpdb->prefix . 'mxchat_system_prompt_content';
430
431 // Fetch the article content and associated product URL
432 $query = $wpdb->prepare("SELECT article_content, source_url FROM {$system_prompt_table} WHERE id = %d", $most_relevant_id);
433 $result = $wpdb->get_row($query);
434
435 if ($result) {
436 // Append the product link to the content if available
437 $content = $result->article_content;
438 if (!empty($result->source_url)) {
439 $content .= "\n\nFor more details, check out this product: " . esc_url($result->source_url);
440 }
441 return $content;
442 }
443
444 return null;
445 }
446
447
448 private function mxchat_generate_response($relevant_content, $api_key, $conversation_history) {
449 if (!$relevant_content) {
450 return "I'm sorry, I couldn't find relevant information on that topic.";
451 }
452
453 $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
454
455 $content_with_instructions = $system_prompt_instructions . " " . $relevant_content;
456
457 array_unshift($conversation_history, [
458 'role' => 'system',
459 'content' => "Here are your instructions: " . $content_with_instructions
460 ]);
461
462 foreach ($conversation_history as &$message) {
463 if ($message['role'] === 'bot') {
464 $message['role'] = 'assistant';
465 }
466 }
467
468 $api_url = 'https://api.openai.com/v1/chat/completions';
469
470 $body = json_encode([
471 'model' => isset($this->options['model']) ? $this->options['model'] : 'gpt-3.5',
472 'messages' => $conversation_history,
473 ]);
474
475 $args = [
476 'body' => $body,
477 'headers' => [
478 'Content-Type' => 'application/json',
479 'Authorization' => 'Bearer ' . $api_key,
480 ],
481 'timeout' => 60,
482 'redirection' => 5,
483 'blocking' => true,
484 'httpversion' => '1.0',
485 'sslverify' => true,
486 ];
487
488 $response = wp_remote_post($api_url, $args);
489
490 if (is_wp_error($response)) {
491 return "Sorry, there was an error processing your request.";
492 }
493
494 $response_body = json_decode(wp_remote_retrieve_body($response), true);
495
496 if (isset($response_body['choices'][0]['message']['content'])) {
497 if (isset($response_body['usage'])) {
498 $prompt_tokens = $response_body['usage']['prompt_tokens'];
499 $total_tokens = $response_body['usage']['total_tokens'];
500 }
501 return trim($response_body['choices'][0]['message']['content']);
502 } else {
503 return "Sorry, I couldn't process that request.";
504 }
505 }
506
507
508 public function mxchat_dismiss_pre_chat_message() {
509 // Get and sanitize the user identifier
510 $user_id = $this->mxchat_get_user_identifier();
511 $user_id = sanitize_key($user_id);
512
513 // Set a transient to track that the user has dismissed the pre-chat message
514 $transient_key = 'mxchat_pre_chat_message_dismissed_' . $user_id;
515 set_transient($transient_key, true, DAY_IN_SECONDS);
516
517 wp_send_json_success();
518 }
519
520
521
522 private function mxchat_calculate_cosine_similarity($vectorA, $vectorB) {
523 if (!is_array($vectorA) || !is_array($vectorB) || empty($vectorA) || empty($vectorB)) {
524 return 0;
525 }
526
527 $dotProduct = array_sum(array_map(function ($a, $b) {
528 return $a * $b;
529 }, $vectorA, $vectorB));
530 $normA = sqrt(array_sum(array_map(function ($a) {
531 return $a * $a;
532 }, $vectorA)));
533 $normB = sqrt(array_sum(array_map(function ($b) {
534 return $b * $b;
535 }, $vectorB)));
536
537 if ($normA == 0 || $normB == 0) {
538 return 0;
539 }
540
541 return $dotProduct / ($normA * $normB);
542 }
543
544 public function mxchat_enqueue_scripts_styles() {
545 // Define version numbers for the styles and scripts
546 $chat_style_version = '1.1.4'; // Replace with your actual version
547 $chat_script_version = '1.1.4'; // Replace with your actual version
548
549 // Correct path to the script file
550 wp_enqueue_script(
551 'mxchat-chat-js', // Handle for the script
552 plugin_dir_url(__FILE__) . '../js/chat-script.js', // Correct path using __FILE__
553 array('jquery'), // Dependencies
554 $chat_script_version, // Version for cache busting
555 true // Load script in footer
556 );
557
558 // Enqueue the CSS file similarly
559 wp_enqueue_style(
560 'mxchat-chat-css', // Handle for the style
561 plugin_dir_url(__FILE__) . '../css/chat-style.css', // Correct path using __FILE__
562 array(), // No dependencies
563 $chat_style_version // Version for cache busting
564 );
565
566 // Fetch options from the database
567 $this->options = get_option('mxchat_options');
568
569 // Prepare settings to pass to JavaScript
570 $style_settings = array(
571 'ajax_url' => admin_url('admin-ajax.php'),
572 'nonce' => wp_create_nonce('mxchat_chat_nonce'), // Nonce for security
573 'rate_limit_message' => $this->options['rate_limit_message'] ?? 'Rate limit exceeded. Please try again later.',
574 'appendWidgetToBody' => $this->options['append_to_body'] ?? 'off'
575 );
576
577 // Localize the script with necessary data
578 wp_localize_script('mxchat-chat-js', 'mxchatChat', $style_settings);
579 }
580
581
582
583 public function mxchat_reset_rate_limits() {
584 global $wpdb;
585
586 // Define a cache key pattern for rate limits
587 $cache_key_pattern = 'mxchat_chat_limit_%';
588
589 // Retrieve all option names matching the pattern
590 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.DirectQuery
591 $option_names = $wpdb->get_col("SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE 'mxchat_chat_limit_%'");
592
593 // db call ok; no-cache ok
594 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- db call ok
595 $wpdb->query("DELETE FROM {$wpdb->options} WHERE option_name LIKE 'mxchat_chat_limit_%'");
596
597 // Clear the relevant cache entries
598 foreach ($option_names as $option_name) {
599 wp_cache_delete($option_name, 'options');
600 }
601
602 // Optionally, clear a general cache if you have one
603 wp_cache_delete('mxchat_all_chat_limits', 'options');
604 }
605
606
607 private function mxchat_fetch_woocommerce_products() {
608 // Ensure WooCommerce is active
609 if (!class_exists('WooCommerce')) {
610 return [];
611 }
612
613 $args = array(
614 'post_type' => 'product',
615 'post_status' => 'publish',
616 'posts_per_page' => -1,
617 );
618
619 $products = get_posts($args);
620 $product_data = [];
621
622 foreach ($products as $product) {
623 $product_id = $product->ID;
624 $product_obj = wc_get_product($product_id);
625
626 $product_data[] = array(
627 'id' => $product_id,
628 'name' => $product_obj->get_name(),
629 'description' => $product_obj->get_description(),
630 'short_description' => $product_obj->get_short_description(),
631 'url' => get_permalink($product_id),
632 'price' => $product_obj->get_regular_price(),
633 'sale_price' => $product_obj->get_sale_price(),
634 'stock_status' => $product_obj->get_stock_status(),
635 'sku' => $product_obj->get_sku(),
636 'in_stock' => $product_obj->is_in_stock(),
637 'total_sales' => $product_obj->get_total_sales(),
638 );
639 }
640
641 return $product_data;
642 }
643
644
645
646
647
648
649
650 }
651 ?>
652