title = __('FluentCart', 'notificationx'); $this->module_title = __('FluentCart', 'notificationx'); } public function init(){ parent::init(); add_action('fluent_cart/order_paid_done', array( $this, 'save_new_records'), 10, 1); add_action('fluent_cart/order_created', array( $this, 'save_new_records'), 10, 1); add_action('fluent_cart/payment_status_changed', [$this, 'status_transition'], 10, 1); add_action('fluent_cart/order_status_changed', [$this, 'status_transition'], 10, 1); } public function init_fields(){ parent::init_fields(); add_filter('nx_link_types', [$this, 'link_types']); add_filter( 'nx_fluentcart_order_status', array( $this, 'order_status' ), 11 ); add_filter("nx_notification_link_{$this->id}", [$this, 'product_link'], 10, 3); add_filter('nx_conversion_category_list', [$this, 'collections']); add_filter('nx_conversion_product_list', [$this, 'product_lists']); } /** * This functions is hooked * * @return void */ public function admin_actions() { parent::admin_actions(); add_filter("nx_can_entry_{$this->id}", array($this, 'nx_can_entry'), 10, 3); } public function public_actions(){ parent::public_actions(); } public function nx_can_entry($return, $entry, $settings){ // Default to 'paid' and 'processing' if no status is specified $allowed_statuses = !empty($settings['fluentcart_order_status']) ? $settings['fluentcart_order_status'] : ['paid', 'processing']; // Check if any of the order's statuses match the allowed statuses $status_matches = false; // Check payment status if (!empty($entry['data']['payment_status']) && in_array($entry['data']['payment_status'], $allowed_statuses)) { $status_matches = true; } // Check order status if (!empty($entry['data']['status']) && in_array($entry['data']['status'], $allowed_statuses)) { $status_matches = true; } // Check shipping status if (!empty($entry['data']['shipping_status']) && in_array($entry['data']['shipping_status'], $allowed_statuses)) { $status_matches = true; } // Check fulfillment status (FluentCart specific) if (!empty($entry['data']['fulfillment_status']) && in_array($entry['data']['fulfillment_status'], $allowed_statuses)) { $status_matches = true; } // Return the original return value if status matches, otherwise return false if (!$status_matches) { return false; } // Apply product / category filter so real-time orders respect each // campaign's Selected Product / Exclude Product settings — matching the // Regenerate path. Operates against the DB-loaded settings shape, where // product_list / exclude_products / category_list / exclude_categories // are flat arrays of slugs (flattened by async_select_remove_label). $product_id = !empty($entry['data']['product_id']) ? $entry['data']['product_id'] : 0; if ($product_id) { $product_slug = get_post_field('post_name', $product_id); $category_slugs = []; $terms = get_the_terms($product_id, 'product-categories'); if (is_array($terms)) { foreach ($terms as $term) { $category_slugs[] = $term->slug; } } // Show-purchase-of filter $control = !empty($settings['product_control']) ? $settings['product_control'] : 'none'; if ($control === 'manual_selection') { $list = !empty($settings['product_list']) ? (array) $settings['product_list'] : []; if (!in_array($product_slug, $list, true)) { return false; } } elseif ($control === 'product_category') { $list = !empty($settings['category_list']) ? (array) $settings['category_list'] : []; if (empty(array_intersect($category_slugs, $list))) { return false; } } // Exclude-product filter $exclude_by = !empty($settings['product_exclude_by']) ? $settings['product_exclude_by'] : 'none'; if ($exclude_by === 'manual_selection') { $list = !empty($settings['exclude_products']) ? (array) $settings['exclude_products'] : []; if (in_array($product_slug, $list, true)) { return false; } } elseif ($exclude_by === 'product_category') { $list = !empty($settings['exclude_categories']) ? (array) $settings['exclude_categories'] : []; if (!empty(array_intersect($category_slugs, $list))) { return false; } } } return $return; } public function product_lists($products) { $all_products = \FluentCart\App\Models\Product::published()->get(); $product_lists = []; if( ! is_wp_error( $all_products ) && $all_products->count() > 0 ) { foreach( $all_products as $product ) { $product_lists[ $product->post_name ] = $product->post_title; } } $options = GlobalFields::get_instance()->normalize_fields($product_lists, 'source', $this->id, $products); return $options; } public function collections($options) { // Get FluentCart product categories using the product-categories taxonomy $categories = get_terms([ 'taxonomy' => 'product-categories', 'hide_empty' => false, ]); $category_list = []; if (!is_wp_error($categories) && !empty($categories)) { foreach ($categories as $category) { $category_list[$category->slug] = $category->name; } } $options = GlobalFields::get_instance()->normalize_fields($category_list, 'source', $this->id, $options); return $options; } public function product_link($link, $post, $entry) { if(!empty($entry['permalink']) && !empty( $post['link_type'] ) && $post['link_type'] === 'product_page' ){ $link = $entry['permalink']; } return $link; } public function order_status($options){ // Get FluentCart's native order and payment statuses $order_status = []; try { // Check if FluentCart Status class is available if (class_exists('\FluentCart\App\Helpers\Status')) { // Get Order Statuses $order_statuses = \FluentCart\App\Helpers\Status::getOrderStatuses(); foreach ($order_statuses as $key => $label) { $order_status[$key] = $label; } // Get Payment Statuses $payment_statuses = \FluentCart\App\Helpers\Status::getPaymentStatuses(); foreach ($payment_statuses as $key => $label) { $order_status[$key] = $label . ' (' . __('Payment', 'notificationx') . ')'; } // Get Shipping Statuses if available $shipping_statuses = \FluentCart\App\Helpers\Status::getShippingStatuses(); foreach ($shipping_statuses as $key => $label) { $order_status[$key] = $label . ' (' . __('Shipping', 'notificationx') . ')'; } } else { // Fallback to manual status list if FluentCart classes are not available $order_status = [ // Order Statuses 'processing' => __( 'Processing','notificationx' ), 'completed' => __( 'Completed','notificationx' ), 'on-hold' => __( 'On Hold','notificationx' ), 'canceled' => __( 'Canceled','notificationx' ), 'failed' => __( 'Failed','notificationx' ), // Payment Statuses 'pending' => __( 'Pending (Payment)','notificationx' ), 'paid' => __( 'Paid (Payment)','notificationx' ), 'partially_paid' => __( 'Partially Paid (Payment)','notificationx' ), 'payment_failed' => __( 'Failed (Payment)','notificationx' ), 'refunded' => __( 'Refunded (Payment)','notificationx' ), 'partially_refunded' => __( 'Partially Refunded (Payment)','notificationx' ), 'authorized' => __( 'Authorized (Payment)','notificationx' ), // Shipping Statuses 'unfulfilled' => __( 'Unfulfilled (Shipping)','notificationx' ), 'fulfilled' => __( 'Fulfilled (Shipping)','notificationx' ), 'shipped' => __( 'Shipped (Shipping)','notificationx' ), 'delivered' => __( 'Delivered (Shipping)','notificationx' ), ]; } } catch (\Exception $e) { // Fallback in case of any errors $order_status = [ 'processing' => __( 'Processing','notificationx' ), 'completed' => __( 'Completed','notificationx' ), 'paid' => __( 'Paid','notificationx' ), 'fulfilled' => __( 'Fulfilled','notificationx' ), ]; } $options = GlobalFields::get_instance()->normalize_fields( $order_status, 'source', $this->id, $options); return $options; } public function save_new_records($data) { $order = isset($data['order']) ? $data['order'] : null; $customer = isset($data['customer']) ? $data['customer'] : null; if (!$order || !$customer) { return; } // Load order items $order->load('order_items'); foreach ($order->order_items as $order_item) { if (!empty($order_item)) { $single_notifications = $this->prepare_order_data($order_item, $customer, [], $order); $key = $order->id . '_' . $order_item->id; $this->save([ 'source' => $this->id, 'entry_key' => $key, 'data' => $single_notifications, ], true); } } } public function save_order_created($data) { // This can be used for order created events if needed // For now, we'll focus on paid orders return; } public function prepare_order_data( $order_item, $customer, $return = [], $order = null ) { $address_fields = ['city','country','address_1','address_2','postcode']; $customer_fields = ['first_name','last_name','email','full_name']; // Get product information from order item if( !empty( $order_item->post_title ) ) { $return['title'] = $order_item->post_title; } if( !empty( $order_item->post_title ) ) { $return['product_title'] = $order_item->post_title; } if( !empty( $order_item->post_id ) ) { $return['product_id'] = $order_item->post_id; // Get product permalink $return['permalink'] = get_permalink($order_item->post_id); // Get product image $return['image_url'] = get_the_post_thumbnail_url($order_item->post_id, 'thumbnail'); } if( !empty( $order_item->unit_price ) ) { $return['price'] = $order_item->unit_price; } if( !empty( $order_item->quantity ) ) { $return['quantity'] = $order_item->quantity; } // Get customer information foreach ($customer_fields as $customer_field) { if( !empty( $customer->{$customer_field} ) ) { $return[$customer_field] = $customer->{$customer_field}; } } // Get address information from order if available if ($order) { $order->load('billing_address', 'shipping_address'); $address = $order->billing_address ?: $order->shipping_address; if ($address) { foreach ($address_fields as $address_field) { if( !empty( $address->{$address_field} ) ) { $return[$address_field] = $address->{$address_field}; } } } // Order information if( !empty( $order->id ) ) { $return['order_id'] = $order->id; } if( !empty( $order->payment_status ) ) { $return['payment_status'] = $order->payment_status; } if( !empty( $order->status ) ) { $return['status'] = $order->status; } if( !empty( $order->shipping_status ) ) { $return['shipping_status'] = $order->shipping_status; } if( !empty( $order->fulfillment_status ) ) { $return['fulfillment_status'] = $order->fulfillment_status; } if( !empty( $order->fulfillment_type ) ) { $return['fulfillment_type'] = $order->fulfillment_type; } if( !empty( $order->created_at ) ) { $return['timestamp'] = $order->created_at; } if( !empty( $order->ip_address ) ) { $return['ip'] = $order->ip_address; } } return $return; } /** * Image action callback * @param array $image_data * @param array $data * @param stdClass $settings * @return array */ public function notification_image($image_data, $data, $settings) { if (!$settings['show_default_image'] && $settings['show_notification_image'] === 'featured_image') { if ( !empty( $data['image_url'] ) ) { $image_data['url'] = $data['image_url']; } } return $image_data; } public function saved_post($post, $data, $nx_id) { $this->delete_notification(null, $nx_id); $this->get_notification_ready($data); } public function get_notification_ready( $post = array() ) { // Validate required parameters if( empty( $post ) || empty( $post['nx_id'] ) ) { return; } // Get orders based on settings $orders = $this->get_orders( $post ); if ( is_array( $orders ) && ! empty( $orders ) ) { $entries = []; foreach ( $orders as $order ) { // Ensure we have all required data if( empty( $order['entry_key'] ) || empty( $order['updated_at'] ) ) { continue; } $entries[] = [ 'nx_id' => $post['nx_id'], 'source' => $this->id, 'entry_key' => $order['entry_key'], // Use the unique entry key we created 'data' => $order, 'updated_at' => Helper::mysql_time($order['updated_at']), ]; } // Only update if we have valid entries if( !empty( $entries ) ) { $this->update_notifications($entries); } } } public function status_transition($data) { $order = isset($data['order']) ? $data['order'] : null; $customer = isset($data['customer']) ? $data['customer'] : null; if (!$order || !$customer) { return $data; } // Find all entries for this order (there can be multiple for different order items) // We need to search for entries that start with the order ID $all_entries = Entries::get_instance()->get_entries(['source' => $this->id]); $order_entries = []; if (!empty($all_entries)) { foreach ($all_entries as $entry) { // Check if entry_key starts with order ID (format: order_id_item_id) if (strpos($entry['entry_key'], $order->id . '_') === 0) { $order_entries[] = $entry; } } } if (!empty($order_entries) && count($order_entries) > 0) { $entries = []; foreach ($order_entries as $existing_order) { $update_data = !empty($existing_order) ? $existing_order : []; if (empty($existing_order) || empty($order)) { continue; } // Update status information if (!empty($order->payment_status)) { $update_data['data']['payment_status'] = $order->payment_status; } if (!empty($order->status)) { $update_data['data']['status'] = $order->status; } if (!empty($order->shipping_status)) { $update_data['data']['shipping_status'] = $order->shipping_status; } $entries[] = [ 'nx_id' => $update_data['nx_id'], 'source' => $this->id, 'entry_key' => $existing_order['entry_key'], // Keep the original entry key 'data' => $update_data['data'], 'updated_at' => Helper::mysql_time($order->updated_at), ]; } // Delete old entries and add updated ones foreach ($order_entries as $existing_order) { $this->delete_notification($existing_order['entry_key'], $existing_order['nx_id']); } $this->update_notifications($entries); } return $data; } public function get_orders( $post = array() ) { if( empty( $post ) ) { return; } $dateFrom = !empty( $post['display_from'] ) ? date('Y-m-d',strtotime('-'.$post['display_from'].' days',time())) : ''; $dateTo = date('Y-m-d',strtotime('1 days',time())); $amount = !empty( $post['display_last'] ) ? $post['display_last'] : 10; $get_orders = \FluentCart\App\Models\Order::with(['customer', 'order_items', 'billing_address', 'shipping_address']) ->orderBy('created_at', 'desc') ->limit($amount) ->get(); $orders = []; if( $get_orders->count() > 0 ) { foreach ($get_orders as $order) { $createdAt = strtotime($order->created_at); if ($createdAt >= strtotime($dateFrom) && $createdAt <= strtotime($dateTo)) { if( !empty( $order->order_items ) && $order->order_items->count() > 0 ) { foreach ($order->order_items as $order_item) { // Get product categories for this order item $categories = []; if (!empty($order_item->post_id)) { $product_categories = get_the_terms($order_item->post_id, 'product-categories'); if (!is_wp_error($product_categories) && !empty($product_categories)) { $categories = $product_categories; } } if(( $this->_excludes_product($order_item, $post, $categories) === $this->_show_purchaseof($order_item, $post, $categories))){ continue; } $make_orders = $this->prepare_order_data($order_item, $order->customer, [], $order); $make_orders['order'] = $order->id; $make_orders['order_item_id'] = $order_item->id; $make_orders['entry_key'] = $order->id . '_' . $order_item->id; $make_orders['updated_at'] = $order->updated_at; $orders[] = $make_orders; } } } } } return $orders; } public function _excludes_product($order_item, $settings, $categories = []) { if( !empty( $settings['product_exclude_by'] ) && $settings['product_exclude_by'] === 'none' ) { if( !empty( $settings['product_control'] ) && $settings['product_control'] === 'none' ) { return true; } return false; } // Check product list if( $settings['product_exclude_by'] == 'manual_selection' && !empty( $settings['exclude_products'] ) && count( $settings['exclude_products'] ) > 0 ) { foreach ( $settings['exclude_products'] as $__product ) { $product_slug = get_post_field('post_name', $order_item->post_id); if( $__product['value'] == $product_slug ) { return true; } } } // Check category list if( $settings['product_exclude_by'] == 'product_category' && !empty( $settings['exclude_categories'] ) && count( $settings['exclude_categories'] ) > 0 ) { foreach ($categories as $category) { if( in_array( $category->slug, $settings['exclude_categories'] ) ) { return true; } } } return false; } public function _show_purchaseof($order_item, $settings, $categories = []) { if( !empty( $settings['product_control'] ) && $settings['product_control'] === 'none' ) { if( !empty( $settings['product_exclude_by'] ) && $settings['product_exclude_by'] === 'none' ) { return false; } return true; } // Check product list if( $settings['product_control'] == 'manual_selection' && !empty( $settings['product_list'] ) && count( $settings['product_list'] ) > 0 ) { foreach ( $settings['product_list'] as $__product ) { $product_slug = get_post_field('post_name', $order_item->post_id); if( $__product['value'] == $product_slug ) { return true; } } return false; } // Check category list if( $settings['product_control'] == 'product_category' && !empty( $settings['category_list'] ) && count( $settings['category_list'] ) > 0 ) { foreach ($categories as $category) { if( in_array( $category->slug, $settings['category_list'] ) ) { return true; } } return false; } return true; } /** * Lists available FluentCart products for search functionality. * * @param array $args An array of arguments, including inputValue. * @return array An indexed array of product IDs and titles. */ public function restResponse($args) { // Check if inputValue is provided or if we should return empty results if ( empty( $args['search_empty']) && empty($args['inputValue'] ) ) { return []; } // Use FluentCart's native search if available, otherwise fallback to WordPress search $products = $this->search_fluentcart_products($args['inputValue']); // If FluentCart search didn't work, fallback to WordPress post search if (empty($products)) { $products = Helper::get_post_titles_by_search($this->post_type, $args['inputValue']); } // Normalize the fields and return as an indexed array return array_values(GlobalFields::get_instance()->normalize_fields($products, 'source', $this->id)); } /** * Search FluentCart products using FluentCart's native methods * * @param string $search_term The search term * @return array Array of products with ID and title */ private function search_fluentcart_products($search_term = '') { $products = []; try { // Check if FluentCart is available if (!class_exists('\FluentCart\App\Models\Product')) { return $products; } // Use FluentCart's Product model to search $query = \FluentCart\App\Models\Product::query() ->where('post_status', 'publish'); // Add search condition if search term is provided if (!empty($search_term)) { $query->where(function($q) use ($search_term) { // Search by product title $q->where('post_title', 'like', '%' . $search_term . '%') // Also search by product content/description ->orWhere('post_content', 'like', '%' . $search_term . '%') // Search by product excerpt ->orWhere('post_excerpt', 'like', '%' . $search_term . '%'); }); } $fluent_products = $query->get(); // Convert to the expected format foreach ($fluent_products as $product) { $products[$product->ID] = $product->post_title; } } catch (\Exception $e) { // If FluentCart search fails, return empty array to trigger fallback return []; } return $products; } /** * Get products by category for enhanced search functionality * * @param string $category_slug The category slug to search for * @return array Array of products in the specified category */ public function get_products_by_category($category_slug = '') { $products = []; if (empty($category_slug)) { return $products; } try { // Get products that belong to the specified category $args = array( 'post_type' => $this->post_type, 'post_status' => 'publish', 'posts_per_page' => 20, 'tax_query' => array( array( 'taxonomy' => 'product-categories', 'field' => 'slug', 'terms' => $category_slug, ), ), ); $query = new \WP_Query($args); if ($query->have_posts()) { while ($query->have_posts()) { $query->the_post(); $products[get_the_ID()] = get_the_title(); } wp_reset_postdata(); } } catch (\Exception $e) { // If category search fails, return empty array return []; } return $products; } /** * @todo remove in the future. * * @param [type] $data * @param [type] $settings * @return void */ public function show_exclude_product( $data, $settings ){ $new_data = []; if( ! empty( $data ) ) { foreach( $data as $key => $product ) { if( $this->_excludes_product($product, $settings) && $this->_show_purchaseof($product, $settings) ) { $new_data[ $key ] = $product; } } } return $new_data; } /** * Adds option to Link Type field in Content tab. * * @param array $options * @return array */ public function link_types($options) { $options = GlobalFields::get_instance()->normalize_fields([ 'product_page' => __('Product Page', 'notificationx'), ], 'source', $this->id, $options); return $options; } public function fallback_data($data, $entry) { $data['name'] = __('Someone', 'notificationx'); $data['first_name'] = __('Someone', 'notificationx'); $data['last_name'] = __('Someone', 'notificationx'); $data['anonymous_title'] = __('Anonymous Product', 'notificationx'); if(empty($entry['product_title']) && !empty($entry['title'])){ $data['product_title'] = $entry['title']; } return $data; } public function source_error_message($messages) { if (!$this->class_exists()) { $url = admin_url('plugin-install.php?s=fluentcart&tab=search&type=term'); $messages[$this->id] = [ 'message' => sprintf( '%s %s %s', __( 'You have to install', 'notificationx' ), $url, __( 'FluentCart', 'notificationx' ), __( 'plugin first.', 'notificationx' ) ), 'html' => true, 'type' => 'error', 'rules' => Rules::is('source', $this->id), ]; } return $messages; } public function doc(){ return sprintf(__('

Make sure that you have the FluentCart WordPress plugin installed & configured to use its campaign and selling data. For detailed guidelines, check out the step-by-step documentation.

👉 NotificationX Integration with FluentCart', 'notificationx'), 'https://wordpress.org/plugins/fluent-cart/', 'https://notificationx.com/docs/fluentcart-sales-notifications/', 'https://notificationx.com/docs/fluentcart-sales-notifications/' ); } }