PluginProbe
NotificationX – FOMO, Live Sales Notification, WooCommerce Sales Popup, GDPR, Social Proof, Announcement Banner & Floating Notification Bar / trunk
NotificationX – FOMO, Live Sales Notification, WooCommerce Sales Popup, GDPR, Social Proof, Announcement Banner & Floating Notification Bar vtrunk
3.3.1 3.3.0 3.2.14 3.2.13 3.2.12 3.2.11 3.2.10 3.2.9 3.2.8 3.2.7 trunk 0.2.5.5 0.2.5.6 0.2.5.7 1.0.0 1.0.1 1.0.2 1.0.3 1.1.0 1.1.1 1.1.2 1.1.3 1.1.4 1.2.0 1.2.1 All 156 releases
notificationx / includes / Extensions / FluentCart / FluentCart.php

FluentCart.php in NotificationX – FOMO, Live Sales Notification, WooCommerce Sales Popup, GDPR, Social Proof, Announcement Banner & Floating Notification Bar trunk, at includes/Extensions/FluentCart/FluentCart.php

776 lines 30.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * FluentCart Extension
4 *
5 * @package NotificationX\Extensions
6 */
7
8 namespace NotificationX\Extensions\FluentCart;
9 use NotificationX\GetInstance;
10 use NotificationX\Extensions\Extension;
11 use NotificationX\Extensions\GlobalFields;
12 use NotificationX\Admin\Entries;
13 use NotificationX\Core\Helper;
14 use NotificationX\Core\Rules;
15
16 /**
17 * FluentCart Extension Class
18 * @method static FluentCart get_instance($args = null)
19 */
20 class FluentCart extends Extension {
21 use GetInstance;
22 public $priority = 8;
23 public $id = 'fluentcart';
24 public $doc_link = 'https://notificationx.com/docs/fluentcart-sales-notifications/';
25 public $types = 'conversions';
26 public $img = NOTIFICATIONX_ADMIN_URL . 'images/extensions/sources/fluentcart.png';
27 public $module = 'modules_fluentcart';
28 public $module_priority = 35;
29 public $class = '\FluentCart\Framework\Foundation\App';
30 public $post_type = 'fluent-products';
31
32 /**
33 * Initially Invoked when initialized.
34 */
35 public function __construct(){
36 parent::__construct();
37 }
38
39 public function init_extension()
40 {
41 $this->title = __('FluentCart', 'notificationx');
42 $this->module_title = __('FluentCart', 'notificationx');
43 }
44
45 public function init(){
46 parent::init();
47 add_action('fluent_cart/order_paid_done', array( $this, 'save_new_records'), 10, 1);
48 add_action('fluent_cart/order_created', array( $this, 'save_new_records'), 10, 1);
49 add_action('fluent_cart/payment_status_changed', [$this, 'status_transition'], 10, 1);
50 add_action('fluent_cart/order_status_changed', [$this, 'status_transition'], 10, 1);
51 }
52
53 public function init_fields(){
54 parent::init_fields();
55 add_filter('nx_link_types', [$this, 'link_types']);
56 add_filter( 'nx_fluentcart_order_status', array( $this, 'order_status' ), 11 );
57 add_filter("nx_notification_link_{$this->id}", [$this, 'product_link'], 10, 3);
58 add_filter('nx_conversion_category_list', [$this, 'collections']);
59 add_filter('nx_conversion_product_list', [$this, 'product_lists']);
60
61 }
62
63 /**
64 * This functions is hooked
65 *
66 * @return void
67 */
68 public function admin_actions() {
69 parent::admin_actions();
70 add_filter("nx_can_entry_{$this->id}", array($this, 'nx_can_entry'), 10, 3);
71 }
72
73 public function public_actions(){
74 parent::public_actions();
75 }
76
77 public function nx_can_entry($return, $entry, $settings){
78 // Default to 'paid' and 'processing' if no status is specified
79 $allowed_statuses = !empty($settings['fluentcart_order_status']) ? $settings['fluentcart_order_status'] : ['paid', 'processing'];
80
81 // Check if any of the order's statuses match the allowed statuses
82 $status_matches = false;
83
84 // Check payment status
85 if (!empty($entry['data']['payment_status']) && in_array($entry['data']['payment_status'], $allowed_statuses)) {
86 $status_matches = true;
87 }
88
89 // Check order status
90 if (!empty($entry['data']['status']) && in_array($entry['data']['status'], $allowed_statuses)) {
91 $status_matches = true;
92 }
93
94 // Check shipping status
95 if (!empty($entry['data']['shipping_status']) && in_array($entry['data']['shipping_status'], $allowed_statuses)) {
96 $status_matches = true;
97 }
98
99 // Check fulfillment status (FluentCart specific)
100 if (!empty($entry['data']['fulfillment_status']) && in_array($entry['data']['fulfillment_status'], $allowed_statuses)) {
101 $status_matches = true;
102 }
103
104 // Return the original return value if status matches, otherwise return false
105 if (!$status_matches) {
106 return false;
107 }
108
109 // Apply product / category filter so real-time orders respect each
110 // campaign's Selected Product / Exclude Product settings — matching the
111 // Regenerate path. Operates against the DB-loaded settings shape, where
112 // product_list / exclude_products / category_list / exclude_categories
113 // are flat arrays of slugs (flattened by async_select_remove_label).
114 $product_id = !empty($entry['data']['product_id']) ? $entry['data']['product_id'] : 0;
115 if ($product_id) {
116 $product_slug = get_post_field('post_name', $product_id);
117 $category_slugs = [];
118 $terms = get_the_terms($product_id, 'product-categories');
119 if (is_array($terms)) {
120 foreach ($terms as $term) {
121 $category_slugs[] = $term->slug;
122 }
123 }
124
125 // Show-purchase-of filter
126 $control = !empty($settings['product_control']) ? $settings['product_control'] : 'none';
127 if ($control === 'manual_selection') {
128 $list = !empty($settings['product_list']) ? (array) $settings['product_list'] : [];
129 if (!in_array($product_slug, $list, true)) {
130 return false;
131 }
132 } elseif ($control === 'product_category') {
133 $list = !empty($settings['category_list']) ? (array) $settings['category_list'] : [];
134 if (empty(array_intersect($category_slugs, $list))) {
135 return false;
136 }
137 }
138
139 // Exclude-product filter
140 $exclude_by = !empty($settings['product_exclude_by']) ? $settings['product_exclude_by'] : 'none';
141 if ($exclude_by === 'manual_selection') {
142 $list = !empty($settings['exclude_products']) ? (array) $settings['exclude_products'] : [];
143 if (in_array($product_slug, $list, true)) {
144 return false;
145 }
146 } elseif ($exclude_by === 'product_category') {
147 $list = !empty($settings['exclude_categories']) ? (array) $settings['exclude_categories'] : [];
148 if (!empty(array_intersect($category_slugs, $list))) {
149 return false;
150 }
151 }
152 }
153
154 return $return;
155 }
156
157 public function product_lists($products) {
158 $all_products = \FluentCart\App\Models\Product::published()->get();
159 $product_lists = [];
160 if( ! is_wp_error( $all_products ) && $all_products->count() > 0 ) {
161 foreach( $all_products as $product ) {
162 $product_lists[ $product->post_name ] = $product->post_title;
163 }
164 }
165 $options = GlobalFields::get_instance()->normalize_fields($product_lists, 'source', $this->id, $products);
166 return $options;
167 }
168
169 public function collections($options) {
170 // Get FluentCart product categories using the product-categories taxonomy
171 $categories = get_terms([
172 'taxonomy' => 'product-categories',
173 'hide_empty' => false,
174 ]);
175
176 $category_list = [];
177 if (!is_wp_error($categories) && !empty($categories)) {
178 foreach ($categories as $category) {
179 $category_list[$category->slug] = $category->name;
180 }
181 }
182
183 $options = GlobalFields::get_instance()->normalize_fields($category_list, 'source', $this->id, $options);
184 return $options;
185 }
186
187 public function product_link($link, $post, $entry) {
188 if(!empty($entry['permalink']) && !empty( $post['link_type'] ) && $post['link_type'] === 'product_page' ){
189 $link = $entry['permalink'];
190 }
191 return $link;
192 }
193
194 public function order_status($options){
195 // Get FluentCart's native order and payment statuses
196 $order_status = [];
197
198 try {
199 // Check if FluentCart Status class is available
200 if (class_exists('\FluentCart\App\Helpers\Status')) {
201 // Get Order Statuses
202 $order_statuses = \FluentCart\App\Helpers\Status::getOrderStatuses();
203 foreach ($order_statuses as $key => $label) {
204 $order_status[$key] = $label;
205 }
206
207 // Get Payment Statuses
208 $payment_statuses = \FluentCart\App\Helpers\Status::getPaymentStatuses();
209 foreach ($payment_statuses as $key => $label) {
210 $order_status[$key] = $label . ' (' . __('Payment', 'notificationx') . ')';
211 }
212
213 // Get Shipping Statuses if available
214 $shipping_statuses = \FluentCart\App\Helpers\Status::getShippingStatuses();
215 foreach ($shipping_statuses as $key => $label) {
216 $order_status[$key] = $label . ' (' . __('Shipping', 'notificationx') . ')';
217 }
218
219 } else {
220 // Fallback to manual status list if FluentCart classes are not available
221 $order_status = [
222 // Order Statuses
223 'processing' => __( 'Processing','notificationx' ),
224 'completed' => __( 'Completed','notificationx' ),
225 'on-hold' => __( 'On Hold','notificationx' ),
226 'canceled' => __( 'Canceled','notificationx' ),
227 'failed' => __( 'Failed','notificationx' ),
228
229 // Payment Statuses
230 'pending' => __( 'Pending (Payment)','notificationx' ),
231 'paid' => __( 'Paid (Payment)','notificationx' ),
232 'partially_paid' => __( 'Partially Paid (Payment)','notificationx' ),
233 'payment_failed' => __( 'Failed (Payment)','notificationx' ),
234 'refunded' => __( 'Refunded (Payment)','notificationx' ),
235 'partially_refunded' => __( 'Partially Refunded (Payment)','notificationx' ),
236 'authorized' => __( 'Authorized (Payment)','notificationx' ),
237
238 // Shipping Statuses
239 'unfulfilled' => __( 'Unfulfilled (Shipping)','notificationx' ),
240 'fulfilled' => __( 'Fulfilled (Shipping)','notificationx' ),
241 'shipped' => __( 'Shipped (Shipping)','notificationx' ),
242 'delivered' => __( 'Delivered (Shipping)','notificationx' ),
243 ];
244 }
245 } catch (\Exception $e) {
246 // Fallback in case of any errors
247 $order_status = [
248 'processing' => __( 'Processing','notificationx' ),
249 'completed' => __( 'Completed','notificationx' ),
250 'paid' => __( 'Paid','notificationx' ),
251 'fulfilled' => __( 'Fulfilled','notificationx' ),
252 ];
253 }
254
255 $options = GlobalFields::get_instance()->normalize_fields( $order_status, 'source', $this->id, $options);
256 return $options;
257 }
258
259 public function save_new_records($data) {
260 $order = isset($data['order']) ? $data['order'] : null;
261 $customer = isset($data['customer']) ? $data['customer'] : null;
262
263 if (!$order || !$customer) {
264 return;
265 }
266
267 // Load order items
268 $order->load('order_items');
269
270 foreach ($order->order_items as $order_item) {
271 if (!empty($order_item)) {
272 $single_notifications = $this->prepare_order_data($order_item, $customer, [], $order);
273 $key = $order->id . '_' . $order_item->id;
274 $this->save([
275 'source' => $this->id,
276 'entry_key' => $key,
277 'data' => $single_notifications,
278 ], true);
279 }
280 }
281 }
282
283 public function save_order_created($data) {
284 // This can be used for order created events if needed
285 // For now, we'll focus on paid orders
286 return;
287 }
288
289 public function prepare_order_data( $order_item, $customer, $return = [], $order = null ) {
290 $address_fields = ['city','country','address_1','address_2','postcode'];
291 $customer_fields = ['first_name','last_name','email','full_name'];
292
293 // Get product information from order item
294 if( !empty( $order_item->post_title ) ) {
295 $return['title'] = $order_item->post_title;
296 }
297 if( !empty( $order_item->post_title ) ) {
298 $return['product_title'] = $order_item->post_title;
299 }
300 if( !empty( $order_item->post_id ) ) {
301 $return['product_id'] = $order_item->post_id;
302 // Get product permalink
303 $return['permalink'] = get_permalink($order_item->post_id);
304 // Get product image
305 $return['image_url'] = get_the_post_thumbnail_url($order_item->post_id, 'thumbnail');
306 }
307 if( !empty( $order_item->unit_price ) ) {
308 $return['price'] = $order_item->unit_price;
309 }
310 if( !empty( $order_item->quantity ) ) {
311 $return['quantity'] = $order_item->quantity;
312 }
313
314 // Get customer information
315 foreach ($customer_fields as $customer_field) {
316 if( !empty( $customer->{$customer_field} ) ) {
317 $return[$customer_field] = $customer->{$customer_field};
318 }
319 }
320
321 // Get address information from order if available
322 if ($order) {
323 $order->load('billing_address', 'shipping_address');
324 $address = $order->billing_address ?: $order->shipping_address;
325 if ($address) {
326 foreach ($address_fields as $address_field) {
327 if( !empty( $address->{$address_field} ) ) {
328 $return[$address_field] = $address->{$address_field};
329 }
330 }
331 }
332
333 // Order information
334 if( !empty( $order->id ) ) {
335 $return['order_id'] = $order->id;
336 }
337 if( !empty( $order->payment_status ) ) {
338 $return['payment_status'] = $order->payment_status;
339 }
340 if( !empty( $order->status ) ) {
341 $return['status'] = $order->status;
342 }
343 if( !empty( $order->shipping_status ) ) {
344 $return['shipping_status'] = $order->shipping_status;
345 }
346 if( !empty( $order->fulfillment_status ) ) {
347 $return['fulfillment_status'] = $order->fulfillment_status;
348 }
349 if( !empty( $order->fulfillment_type ) ) {
350 $return['fulfillment_type'] = $order->fulfillment_type;
351 }
352 if( !empty( $order->created_at ) ) {
353 $return['timestamp'] = $order->created_at;
354 }
355 if( !empty( $order->ip_address ) ) {
356 $return['ip'] = $order->ip_address;
357 }
358 }
359
360 return $return;
361 }
362
363 /**
364 * Image action callback
365 * @param array $image_data
366 * @param array $data
367 * @param stdClass $settings
368 * @return array
369 */
370 public function notification_image($image_data, $data, $settings) {
371 if (!$settings['show_default_image'] && $settings['show_notification_image'] === 'featured_image') {
372 if ( !empty( $data['image_url'] ) ) {
373 $image_data['url'] = $data['image_url'];
374 }
375 }
376 return $image_data;
377 }
378
379 public function saved_post($post, $data, $nx_id) {
380 $this->delete_notification(null, $nx_id);
381 $this->get_notification_ready($data);
382 }
383
384 public function get_notification_ready( $post = array() ) {
385 // Validate required parameters
386 if( empty( $post ) || empty( $post['nx_id'] ) ) {
387 return;
388 }
389
390 // Get orders based on settings
391 $orders = $this->get_orders( $post );
392 if ( is_array( $orders ) && ! empty( $orders ) ) {
393 $entries = [];
394 foreach ( $orders as $order ) {
395 // Ensure we have all required data
396 if( empty( $order['entry_key'] ) || empty( $order['updated_at'] ) ) {
397 continue;
398 }
399
400 $entries[] = [
401 'nx_id' => $post['nx_id'],
402 'source' => $this->id,
403 'entry_key' => $order['entry_key'], // Use the unique entry key we created
404 'data' => $order,
405 'updated_at' => Helper::mysql_time($order['updated_at']),
406 ];
407 }
408
409 // Only update if we have valid entries
410 if( !empty( $entries ) ) {
411 $this->update_notifications($entries);
412 }
413 }
414 }
415
416 public function status_transition($data) {
417 $order = isset($data['order']) ? $data['order'] : null;
418 $customer = isset($data['customer']) ? $data['customer'] : null;
419
420 if (!$order || !$customer) {
421 return $data;
422 }
423
424 // Find all entries for this order (there can be multiple for different order items)
425 // We need to search for entries that start with the order ID
426 $all_entries = Entries::get_instance()->get_entries(['source' => $this->id]);
427 $order_entries = [];
428
429 if (!empty($all_entries)) {
430 foreach ($all_entries as $entry) {
431 // Check if entry_key starts with order ID (format: order_id_item_id)
432 if (strpos($entry['entry_key'], $order->id . '_') === 0) {
433 $order_entries[] = $entry;
434 }
435 }
436 }
437
438 if (!empty($order_entries) && count($order_entries) > 0) {
439 $entries = [];
440 foreach ($order_entries as $existing_order) {
441 $update_data = !empty($existing_order) ? $existing_order : [];
442 if (empty($existing_order) || empty($order)) {
443 continue;
444 }
445
446 // Update status information
447 if (!empty($order->payment_status)) {
448 $update_data['data']['payment_status'] = $order->payment_status;
449 }
450 if (!empty($order->status)) {
451 $update_data['data']['status'] = $order->status;
452 }
453 if (!empty($order->shipping_status)) {
454 $update_data['data']['shipping_status'] = $order->shipping_status;
455 }
456
457 $entries[] = [
458 'nx_id' => $update_data['nx_id'],
459 'source' => $this->id,
460 'entry_key' => $existing_order['entry_key'], // Keep the original entry key
461 'data' => $update_data['data'],
462 'updated_at' => Helper::mysql_time($order->updated_at),
463 ];
464 }
465
466 // Delete old entries and add updated ones
467 foreach ($order_entries as $existing_order) {
468 $this->delete_notification($existing_order['entry_key'], $existing_order['nx_id']);
469 }
470 $this->update_notifications($entries);
471 }
472
473 return $data;
474 }
475
476 public function get_orders( $post = array() ) {
477 if( empty( $post ) ) {
478 return;
479 }
480
481 $dateFrom = !empty( $post['display_from'] ) ? gmdate('Y-m-d',strtotime('-'.$post['display_from'].' days',time())) : '';
482 $dateTo = gmdate('Y-m-d',strtotime('1 days',time()));
483 $amount = !empty( $post['display_last'] ) ? $post['display_last'] : 10;
484
485 $get_orders = \FluentCart\App\Models\Order::with(['customer', 'order_items', 'billing_address', 'shipping_address'])
486 ->orderBy('created_at', 'desc')
487 ->limit($amount)
488 ->get();
489
490 $orders = [];
491 if( $get_orders->count() > 0 ) {
492 foreach ($get_orders as $order) {
493 $createdAt = strtotime($order->created_at);
494 if ($createdAt >= strtotime($dateFrom) && $createdAt <= strtotime($dateTo)) {
495 if( !empty( $order->order_items ) && $order->order_items->count() > 0 ) {
496 foreach ($order->order_items as $order_item) {
497 // Get product categories for this order item
498 $categories = [];
499 if (!empty($order_item->post_id)) {
500 $product_categories = get_the_terms($order_item->post_id, 'product-categories');
501 if (!is_wp_error($product_categories) && !empty($product_categories)) {
502 $categories = $product_categories;
503 }
504 }
505
506 if(( $this->_excludes_product($order_item, $post, $categories) === $this->_show_purchaseof($order_item, $post, $categories))){
507 continue;
508 }
509
510 $make_orders = $this->prepare_order_data($order_item, $order->customer, [], $order);
511 $make_orders['order'] = $order->id;
512 $make_orders['order_item_id'] = $order_item->id;
513 $make_orders['entry_key'] = $order->id . '_' . $order_item->id;
514 $make_orders['updated_at'] = $order->updated_at;
515
516 $orders[] = $make_orders;
517 }
518 }
519 }
520 }
521 }
522
523 return $orders;
524 }
525
526 public function _excludes_product($order_item, $settings, $categories = []) {
527 if( !empty( $settings['product_exclude_by'] ) && $settings['product_exclude_by'] === 'none' ) {
528 if( !empty( $settings['product_control'] ) && $settings['product_control'] === 'none' ) {
529 return true;
530 }
531 return false;
532 }
533 // Check product list
534 if( $settings['product_exclude_by'] == 'manual_selection' && !empty( $settings['exclude_products'] ) && count( $settings['exclude_products'] ) > 0 ) {
535 foreach ( $settings['exclude_products'] as $__product ) {
536 $product_slug = get_post_field('post_name', $order_item->post_id);
537 if( $__product['value'] == $product_slug ) {
538 return true;
539 }
540 }
541 }
542 // Check category list
543 if( $settings['product_exclude_by'] == 'product_category' && !empty( $settings['exclude_categories'] ) && count( $settings['exclude_categories'] ) > 0 ) {
544 foreach ($categories as $category) {
545 if( in_array( $category->slug, $settings['exclude_categories'] ) ) {
546 return true;
547 }
548 }
549 }
550 return false;
551 }
552
553 public function _show_purchaseof($order_item, $settings, $categories = []) {
554 if( !empty( $settings['product_control'] ) && $settings['product_control'] === 'none' ) {
555 if( !empty( $settings['product_exclude_by'] ) && $settings['product_exclude_by'] === 'none' ) {
556 return false;
557 }
558 return true;
559 }
560 // Check product list
561 if( $settings['product_control'] == 'manual_selection' && !empty( $settings['product_list'] ) && count( $settings['product_list'] ) > 0 ) {
562 foreach ( $settings['product_list'] as $__product ) {
563 $product_slug = get_post_field('post_name', $order_item->post_id);
564 if( $__product['value'] == $product_slug ) {
565 return true;
566 }
567 }
568 return false;
569 }
570 // Check category list
571 if( $settings['product_control'] == 'product_category' && !empty( $settings['category_list'] ) && count( $settings['category_list'] ) > 0 ) {
572 foreach ($categories as $category) {
573 if( in_array( $category->slug, $settings['category_list'] ) ) {
574 return true;
575 }
576 }
577 return false;
578 }
579 return true;
580 }
581
582 /**
583 * Lists available FluentCart products for search functionality.
584 *
585 * @param array $args An array of arguments, including inputValue.
586 * @return array An indexed array of product IDs and titles.
587 */
588 public function restResponse($args) {
589 // Check if inputValue is provided or if we should return empty results
590 if ( empty( $args['search_empty']) && empty($args['inputValue'] ) ) {
591 return [];
592 }
593
594 // Use FluentCart's native search if available, otherwise fallback to WordPress search
595 $products = $this->search_fluentcart_products($args['inputValue']);
596
597 // If FluentCart search didn't work, fallback to WordPress post search
598 if (empty($products)) {
599 $products = Helper::get_post_titles_by_search($this->post_type, $args['inputValue']);
600 }
601
602 // Normalize the fields and return as an indexed array
603 return array_values(GlobalFields::get_instance()->normalize_fields($products, 'source', $this->id));
604 }
605
606 /**
607 * Search FluentCart products using FluentCart's native methods
608 *
609 * @param string $search_term The search term
610 * @return array Array of products with ID and title
611 */
612 private function search_fluentcart_products($search_term = '') {
613 $products = [];
614
615 try {
616 // Check if FluentCart is available
617 if (!class_exists('\FluentCart\App\Models\Product')) {
618 return $products;
619 }
620
621 // Use FluentCart's Product model to search
622 $query = \FluentCart\App\Models\Product::query()
623 ->where('post_status', 'publish');
624
625 // Add search condition if search term is provided
626 if (!empty($search_term)) {
627 $query->where(function($q) use ($search_term) {
628 // Search by product title
629 $q->where('post_title', 'like', '%' . $search_term . '%')
630 // Also search by product content/description
631 ->orWhere('post_content', 'like', '%' . $search_term . '%')
632 // Search by product excerpt
633 ->orWhere('post_excerpt', 'like', '%' . $search_term . '%');
634 });
635 }
636
637 $fluent_products = $query->get();
638
639 // Convert to the expected format
640 foreach ($fluent_products as $product) {
641 $products[$product->ID] = $product->post_title;
642 }
643
644 } catch (\Exception $e) {
645 // If FluentCart search fails, return empty array to trigger fallback
646 return [];
647 }
648
649 return $products;
650 }
651
652 /**
653 * Get products by category for enhanced search functionality
654 *
655 * @param string $category_slug The category slug to search for
656 * @return array Array of products in the specified category
657 */
658 public function get_products_by_category($category_slug = '') {
659 $products = [];
660
661 if (empty($category_slug)) {
662 return $products;
663 }
664
665 try {
666 // Get products that belong to the specified category
667 $args = array(
668 'post_type' => $this->post_type,
669 'post_status' => 'publish',
670 'posts_per_page' => 20,
671 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query -- False positive: the query is prepared via $this->wpdb->prepare(), which this sniff does not recognise, and only $wpdb->prefix table names are interpolated. Audited 2026-07-16.
672 'tax_query' => array(
673 array(
674 'taxonomy' => 'product-categories',
675 'field' => 'slug',
676 'terms' => $category_slug,
677 ),
678 ),
679 );
680
681 $query = new \WP_Query($args);
682
683 if ($query->have_posts()) {
684 while ($query->have_posts()) {
685 $query->the_post();
686 $products[get_the_ID()] = get_the_title();
687 }
688 wp_reset_postdata();
689 }
690
691 } catch (\Exception $e) {
692 // If category search fails, return empty array
693 return [];
694 }
695
696 return $products;
697 }
698
699 /**
700 * @todo remove in the future.
701 *
702 * @param [type] $data
703 * @param [type] $settings
704 * @return void
705 */
706 public function show_exclude_product( $data, $settings ){
707 $new_data = [];
708
709 if( ! empty( $data ) ) {
710 foreach( $data as $key => $product ) {
711 if( $this->_excludes_product($product, $settings) && $this->_show_purchaseof($product, $settings) ) {
712 $new_data[ $key ] = $product;
713 }
714 }
715 }
716
717 return $new_data;
718
719 }
720
721 /**
722 * Adds option to Link Type field in Content tab.
723 *
724 * @param array $options
725 * @return array
726 */
727 public function link_types($options) {
728 $options = GlobalFields::get_instance()->normalize_fields([
729 'product_page' => __('Product Page', 'notificationx'),
730 ], 'source', $this->id, $options);
731
732 return $options;
733 }
734
735 public function fallback_data($data, $entry) {
736 $data['name'] = __('Someone', 'notificationx');
737 $data['first_name'] = __('Someone', 'notificationx');
738 $data['last_name'] = __('Someone', 'notificationx');
739 $data['anonymous_title'] = __('Anonymous Product', 'notificationx');
740 if(empty($entry['product_title']) && !empty($entry['title'])){
741 $data['product_title'] = $entry['title'];
742 }
743 return $data;
744 }
745
746 public function source_error_message($messages) {
747 if (!$this->class_exists()) {
748 $url = admin_url('plugin-install.php?s=fluentcart&tab=search&type=term');
749 $messages[$this->id] = [
750 'message' => sprintf( '%s <a href="%s" target="_blank">%s</a> %s',
751 __( 'You have to install', 'notificationx' ),
752 $url,
753 __( 'FluentCart', 'notificationx' ),
754 __( 'plugin first.', 'notificationx' )
755 ),
756 'html' => true,
757 'type' => 'error',
758 'rules' => Rules::is('source', $this->id),
759 ];
760 }
761 return $messages;
762 }
763
764 public function doc(){
765 /* translators: %1$s: FluentCart WordPress plugin installed & configured link URL, %2$s: documentation link URL, %3$s: 👉 NotificationX Integration with FluentCart link URL */
766 return sprintf(__('<p>Make sure that you have the <a target="_blank" href="%1$s">FluentCart WordPress plugin installed & configured</a> to use its campaign and selling data. For detailed guidelines, check out the step-by-step <a target="_blank" href="%2$s">documentation</a>.</p>
767 <a target="_blank" href="%3$s">👉 NotificationX Integration with FluentCart</a>', 'notificationx'),
768 'https://wordpress.org/plugins/fluent-cart/',
769 'https://notificationx.com/docs/fluentcart-sales-notifications/',
770 'https://notificationx.com/docs/fluentcart-sales-notifications/'
771 );
772 }
773
774 }
775
776