diff --git a/composer.json b/composer.json
index 00a990486c..0f17c6d53b 100644
--- a/composer.json
+++ b/composer.json
@@ -36,7 +36,8 @@
},
"autoload": {
"psr-4": {
- "WeDevs\\Dokan\\": "includes/"
+ "WeDevs\\Dokan\\": "includes/",
+ "WeDevs\\Dokan\\ThirdParty\\Packages\\": "lib/packages/"
},
"files": [
"includes/functions-rest-api.php",
diff --git a/docs/analytics/reports.md b/docs/analytics/reports.md
new file mode 100644
index 0000000000..0b7b07ef2a
--- /dev/null
+++ b/docs/analytics/reports.md
@@ -0,0 +1,19 @@
+- [Introduction](#introduction)
+- [Custom Products Stats Datastore](#custom-products-stats-datastore)
+
+## Introduction
+To handle **Dokan Orders**, we followed the [WooCommerce Admin Reports Extension Guidelines](https://github.com/woocommerce/woocommerce/blob/trunk/docs/reporting/extending-woocommerce-admin-reports.md#handle-currency-parameters-on-the-server).
+
+## Custom Stats Datastore
+
+We need to customize the default *WooCommerce Analytics Datastore* for some reports. For example, we replaced the [WC Products Stats DataStore](https://github.com/woocommerce/woocommerce/blob/9297409c5a705d1cd0ae65ec9b058271bd90851e/plugins/woocommerce/src/Admin/API/Reports/Products/Stats/DataStore.php#L170) with the [Dokan Product Stats Store](./../../includes/Analytics/Reports/Products/Stats/WcDataStore.php). This modification involves overriding the `$total_query` and `$interval_query` properties by substituting the `Automattic\WooCommerce\Admin\API\Reports\SqlQuery` class with `WeDevs\Dokan\Analytics\Reports\WcSqlQuery`.
+
+The primary change was to update the `get_sql_clause( $type, $handling = 'unfiltered' )` method to `get_sql_clause( $type, $handling = '' )`, allowing us to apply necessary filters for adding JOIN and WHERE clauses to the `dokan_order_stats` table.
+
+### Implementation Steps
+
+- **Step 1:** Create the [WcSqlQuery](./../../includes/Analytics/Reports/DataStoreModifier.php) class to override the `get_sql_clause( $type, $handling = 'unfiltered' )` method from the [WC SqlQuery](https://github.com/woocommerce/woocommerce/blob/9297409c5a705d1cd0ae65ec9b058271bd90851e/plugins/woocommerce/src/Admin/API/Reports/SqlQuery.php#L87) class. The new method should use `get_sql_clause( $type, $handling = '' )`.
+
+- **Step 2:** Implement the [WcDataStore](https://github.com/woocommerce/woocommerce/blob/9297409c5a705d1cd0ae65ec9b058271bd90851e/plugins/woocommerce/src/Admin/API/Reports/Products/Stats/DataStore.php#L170) class to set the `$total_query` and `$interval_query` properties with instance of **WcSqlQuery**.
+
+- **Step 3:** Use the `woocommerce_data_stores` filter within the [DataStoreModifier](./../../includes/Analytics/Reports/DataStoreModifier.php) class to replace the default WooCommerce Products Stats datastore with the custom Dokan Product Stats Store.
\ No newline at end of file
diff --git a/docs/tdd/get-started.md b/docs/tdd/get-started.md
index 28850f0cd7..32bdd47483 100644
--- a/docs/tdd/get-started.md
+++ b/docs/tdd/get-started.md
@@ -301,4 +301,4 @@ $array = [
// Use the custom assertion method
$this->assertNestedContains( [ 'subkey1' => 'value1' ], $array );
$this->assertNestedContains( [ 'key2' => 'value3' ], $array );
-```
\ No newline at end of file
+```
diff --git a/dokan-class.php b/dokan-class.php
new file mode 100755
index 0000000000..12392395ad
--- /dev/null
+++ b/dokan-class.php
@@ -0,0 +1,482 @@
+define_constants();
+
+ register_activation_hook( DOKAN_FILE, [ $this, 'activate' ] );
+ register_deactivation_hook( DOKAN_FILE, [ $this, 'deactivate' ] );
+
+ add_action( 'before_woocommerce_init', [ $this, 'declare_woocommerce_feature_compatibility' ] );
+ add_action( 'woocommerce_loaded', [ $this, 'init_plugin' ] );
+ add_action( 'woocommerce_flush_rewrite_rules', [ $this, 'flush_rewrite_rules' ] );
+
+ // Register admin notices to container and load notices
+ $this->get_container()->get( 'admin_notices' );
+
+ $this->init_appsero_tracker();
+
+ add_action( 'plugins_loaded', [ $this, 'woocommerce_not_loaded' ], 11 );
+ }
+
+ /**
+ * Initializes the WeDevs_Dokan() class
+ *
+ * Checks for an existing WeDevs_WeDevs_Dokan() instance
+ * and if it doesn't find one, create it.
+ */
+ public static function init() {
+ if ( self::$instance === null ) {
+ self::$instance = new self();
+ }
+
+ return self::$instance;
+ }
+
+ /**
+ * Magic getter to bypass referencing objects
+ *
+ * @since 2.6.10
+ *
+ * @param string $prop
+ *
+ * @return object Class Instance
+ */
+ public function __get( $prop ) {
+ if ( $this->get_container()->has( $prop ) ) {
+ return $this->get_container()->get( $prop );
+ }
+
+ if ( array_key_exists( $prop, $this->legacy_container ) ) {
+ return $this->legacy_container[ $prop ];
+ }
+ }
+
+ /**
+ * Check if the PHP version is supported
+ *
+ * @return bool
+ */
+ public function is_supported_php() {
+ if ( version_compare( PHP_VERSION, $this->min_php, '<=' ) ) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Get the plugin path.
+ *
+ * @return string
+ */
+ public function plugin_path() {
+ return untrailingslashit( plugin_dir_path( __FILE__ ) );
+ }
+
+ /**
+ * Get the template path.
+ *
+ * @return string
+ */
+ public function template_path() {
+ return apply_filters( 'dokan_template_path', 'dokan/' );
+ }
+
+ /**
+ * Placeholder for activation function
+ *
+ * Nothing being called here yet.
+ */
+ public function activate() {
+ if ( ! $this->has_woocommerce() ) {
+ set_transient( 'dokan_wc_missing_notice', true );
+ }
+
+ if ( ! $this->is_supported_php() ) {
+ require_once WC_ABSPATH . 'includes/wc-notice-functions.php';
+
+ /* translators: 1: Required PHP Version 2: Running php version */
+ wc_print_notice( sprintf( __( 'The Minimum PHP Version Requirement for Dokan is %1$s. You are Running PHP %2$s', 'dokan-lite' ), $this->min_php, phpversion() ), 'error' );
+ exit;
+ }
+
+ require_once __DIR__ . '/includes/functions.php';
+ require_once __DIR__ . '/includes/functions-compatibility.php';
+
+ $this->get_container()->get( 'upgrades' );
+ $installer = new \WeDevs\Dokan\Install\Installer();
+ $installer->do_install();
+
+ // rewrite rules during dokan activation
+ if ( $this->has_woocommerce() ) {
+ $this->flush_rewrite_rules();
+ }
+ }
+
+ /**
+ * Flush rewrite rules after dokan is activated or woocommerce is activated
+ *
+ * @since 3.2.8
+ */
+ public function flush_rewrite_rules() {
+ // fix rewrite rules
+ $this->get_container()->get( 'rewrite' )->register_rule();
+ flush_rewrite_rules();
+ }
+
+ /**
+ * Placeholder for deactivation function
+ *
+ * Nothing being called here yet.
+ */
+ public function deactivate() {
+ delete_transient( 'dokan_wc_missing_notice', true );
+ }
+
+ /**
+ * Initialize plugin for localization
+ *
+ * @uses load_plugin_textdomain()
+ */
+ public function localization_setup() {
+ load_plugin_textdomain( 'dokan-lite', false, dirname( plugin_basename( __FILE__ ) ) . '/languages/' );
+ }
+
+ /**
+ * Define all constants
+ *
+ * @return void
+ */
+ public function define_constants() {
+ defined( 'DOKAN_PLUGIN_VERSION' ) || define( 'DOKAN_PLUGIN_VERSION', $this->version );
+ defined( 'DOKAN_DIR' ) || define( 'DOKAN_DIR', __DIR__ );
+ defined( 'DOKAN_INC_DIR' ) || define( 'DOKAN_INC_DIR', __DIR__ . '/includes' );
+ defined( 'DOKAN_LIB_DIR' ) || define( 'DOKAN_LIB_DIR', __DIR__ . '/lib' );
+ defined( 'DOKAN_PLUGIN_ASSEST' ) || define( 'DOKAN_PLUGIN_ASSEST', plugins_url( 'assets', __FILE__ ) );
+
+ // give a way to turn off loading styles and scripts from parent theme
+ defined( 'DOKAN_LOAD_STYLE' ) || define( 'DOKAN_LOAD_STYLE', true );
+ defined( 'DOKAN_LOAD_SCRIPTS' ) || define( 'DOKAN_LOAD_SCRIPTS', true );
+ }
+
+ /**
+ * Add High Performance Order Storage Support
+ *
+ * @since 3.8.0
+ *
+ * @return void
+ */
+ public function declare_woocommerce_feature_compatibility() {
+ if ( class_exists( \Automattic\WooCommerce\Utilities\FeaturesUtil::class ) ) {
+ \Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility( 'custom_order_tables', __FILE__, true );
+ \Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility( 'cart_checkout_blocks', __FILE__, true );
+ }
+ }
+
+ /**
+ * Load the plugin after WP User Frontend is loaded
+ *
+ * @return void
+ */
+ public function init_plugin() {
+ $this->includes();
+ $this->init_hooks();
+
+ do_action( 'dokan_loaded' );
+ }
+
+ /**
+ * Initialize the actions
+ *
+ * @return void
+ */
+ public function init_hooks() {
+ // Localize our plugin
+ add_action( 'init', [ $this, 'localization_setup' ] );
+
+ // initialize the classes
+ add_action( 'init', [ $this, 'init_classes' ], 4 );
+ add_action( 'init', [ $this, 'wpdb_table_shortcuts' ], 1 );
+
+ add_action( 'plugins_loaded', [ $this, 'after_plugins_loaded' ] );
+
+ add_filter( 'plugin_action_links_' . plugin_basename( __FILE__ ), [ $this, 'plugin_action_links' ] );
+ add_action( 'in_plugin_update_message-dokan-lite/dokan.php', [ \WeDevs\Dokan\Install\Installer::class, 'in_plugin_update_message' ] );
+
+ add_action( 'widgets_init', [ $this, 'register_widgets' ] );
+ }
+
+ /**
+ * Include all the required files
+ *
+ * @return void
+ */
+ public function includes() {
+ require_once DOKAN_DIR . '/deprecated/deprecated-functions.php';
+ require_once DOKAN_DIR . '/deprecated/deprecated-hooks.php';
+ require_once DOKAN_INC_DIR . '/functions.php';
+
+ if ( ! function_exists( 'dokan_pro' ) ) {
+ require_once DOKAN_INC_DIR . '/reports.php';
+ }
+
+ require_once DOKAN_INC_DIR . '/Order/functions.php';
+ require_once DOKAN_INC_DIR . '/Product/functions.php';
+ require_once DOKAN_INC_DIR . '/Withdraw/functions.php';
+ require_once DOKAN_INC_DIR . '/functions-compatibility.php';
+ require_once DOKAN_INC_DIR . '/wc-functions.php';
+
+ require_once DOKAN_INC_DIR . '/wc-template.php';
+ require_once DOKAN_DIR . '/deprecated/deprecated-classes.php';
+
+ if ( is_admin() ) {
+ require_once DOKAN_INC_DIR . '/Admin/functions.php';
+ } else {
+ require_once DOKAN_INC_DIR . '/template-tags.php';
+ }
+
+ require_once DOKAN_INC_DIR . '/store-functions.php';
+ }
+
+ /**
+ * Init all the classes
+ *
+ * @return void
+ */
+ public function init_classes() {
+ $common_services = $this->get_container()->get( 'common-service' );
+
+ if ( is_admin() ) {
+ $admin_services = $this->get_container()->get( 'admin-service' );
+ } else {
+ $frontend_services = $this->get_container()->get( 'frontend-service' );
+ }
+
+ $container_services = $this->get_container()->get( 'container-service' );
+
+ $this->legacy_container = apply_filters( 'dokan_get_class_container', $this->legacy_container );
+
+ if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) {
+ $ajax_services = $this->get_container()->get( 'ajax-service' );
+ }
+ }
+
+ /**
+ * Load table prefix for withdraw and orders table
+ *
+ * @since 1.0
+ *
+ * @return void
+ */
+ public function wpdb_table_shortcuts() {
+ global $wpdb;
+
+ $wpdb->dokan_withdraw = $wpdb->prefix . 'dokan_withdraw';
+ $wpdb->dokan_orders = $wpdb->prefix . 'dokan_orders';
+ $wpdb->dokan_announcement = $wpdb->prefix . 'dokan_announcement';
+ $wpdb->dokan_refund = $wpdb->prefix . 'dokan_refund';
+ $wpdb->dokan_vendor_balance = $wpdb->prefix . 'dokan_vendor_balance';
+ }
+
+ /**
+ * Executed after all plugins are loaded
+ *
+ * At this point Dokan Pro is loaded
+ *
+ * @since 2.8.7
+ *
+ * @return void
+ */
+ public function after_plugins_loaded() {
+ // Initiate background processes
+ $processes = get_option( 'dokan_background_processes', [] );
+
+ if ( ! empty( $processes ) ) {
+ $update = false;
+ foreach ( $processes as $processor => $file ) {
+ if ( file_exists( $file ) ) {
+ include_once $file;
+ new $processor();
+ } else {
+ $update = true;
+ unset( $processes[ $processor ] );
+ }
+ }
+ if ( $update ) {
+ update_option( 'dokan_background_processes', $processes );
+ }
+ }
+ }
+
+ /**
+ * Register widgets
+ *
+ * @since 2.8
+ *
+ * @return void
+ */
+ public function register_widgets() {
+ $this->get_container()->get( 'widgets' );
+ }
+
+ /**
+ * Returns if the plugin is in PRO version
+ *
+ * @since 2.4
+ *
+ * @return bool
+ */
+ public function is_pro_exists() {
+ return apply_filters( 'dokan_is_pro_exists', false );
+ }
+
+ /**
+ * Plugin action links
+ *
+ * @param array $links
+ *
+ * @since 2.4
+ *
+ * @return array
+ */
+ public function plugin_action_links( $links ) {
+ if ( ! $this->is_pro_exists() ) {
+ $links[] = '' . __( 'Get Pro', 'dokan-lite' ) . '';
+ }
+
+ $links[] = '' . __( 'Settings', 'dokan-lite' ) . '';
+ $links[] = '' . __( 'Documentation', 'dokan-lite' ) . '';
+
+ return $links;
+ }
+
+ /**
+ * Initialize Appsero Tracker
+ *
+ * @return void
+ */
+ public function init_appsero_tracker() {
+ $this->get_container()->get( 'tracker' );
+ }
+
+ /**
+ * Check whether woocommerce is installed and active
+ *
+ * @since 2.9.16
+ *
+ * @return bool
+ */
+ public function has_woocommerce() {
+ return class_exists( 'WooCommerce' );
+ }
+
+ /**
+ * Check whether woocommerce is installed
+ *
+ * @since 3.2.8
+ *
+ * @return bool
+ */
+ public function is_woocommerce_installed() {
+ return in_array( 'woocommerce/woocommerce.php', array_keys( get_plugins() ), true );
+ }
+
+ /**
+ * Handles scenerios when WooCommerce is not active
+ *
+ * @since 2.9.27
+ *
+ * @return void
+ */
+ public function woocommerce_not_loaded() {
+ if ( did_action( 'woocommerce_loaded' ) || ! is_admin() ) {
+ return;
+ }
+
+ require_once DOKAN_INC_DIR . '/functions.php';
+
+ if ( get_transient( '_dokan_setup_page_redirect' ) ) {
+ dokan_redirect_to_admin_setup_wizard();
+ }
+
+ new \WeDevs\Dokan\Admin\SetupWizardNoWC();
+ }
+
+ /**
+ * Get Dokan db version key
+ *
+ * @since 3.0.0
+ *
+ * @return string
+ */
+ public function get_db_version_key() {
+ return $this->db_version_key;
+ }
+
+ public function get_container(): Container {
+ return dokan_get_container();
+ }
+}
diff --git a/dokan.php b/dokan.php
index cbf4cc802c..71d5c57270 100755
--- a/dokan.php
+++ b/dokan.php
@@ -45,551 +45,44 @@
exit;
}
-/**
- * WeDevs_Dokan class
- *
- * @class WeDevs_Dokan The class that holds the entire WeDevs_Dokan plugin
- *
- * @property WeDevs\Dokan\Commission $commission Instance of Commission class
- * @property WeDevs\Dokan\Order\Manager $order Instance of Order Manager class
- * @property WeDevs\Dokan\Product\Manager $product Instance of Order Manager class
- * @property WeDevs\Dokan\Vendor\Manager $vendor Instance of Vendor Manager Class
- * @property WeDevs\Dokan\BackgroundProcess\Manager $bg_process Instance of WeDevs\Dokan\BackgroundProcess\Manager class
- * @property WeDevs\Dokan\Withdraw\Manager $withdraw Instance of WeDevs\Dokan\Withdraw\Manager class
- * @property WeDevs\Dokan\Frontend\Frontend $frontend_manager Instance of \WeDevs\Dokan\Frontend\Frontend class
- * @property WeDevs\Dokan\Registration $registration Instance of WeDevs\Dokan\Registration class
- */
-final class WeDevs_Dokan {
-
- /**
- * Plugin version
- *
- * @var string
- */
- public $version = '3.11.5';
-
- /**
- * Instance of self
- *
- * @var WeDevs_Dokan
- */
- private static $instance = null;
-
- /**
- * Minimum PHP version required
- *
- * @var string
- */
- private $min_php = '7.4';
-
- /**
- * Holds various class instances
- *
- * @since 2.6.10
- *
- * @var array
- */
- private $container = [];
-
- /**
- * Databse version key
- *
- * @since 3.0.0
- *
- * @var string
- */
- private $db_version_key = 'dokan_theme_version';
-
- /**
- * Constructor for the WeDevs_Dokan class
- *
- * Sets up all the appropriate hooks and actions
- * within our plugin.
- */
- private function __construct() {
- require_once __DIR__ . '/vendor/autoload.php';
-
- $this->define_constants();
-
- register_activation_hook( __FILE__, [ $this, 'activate' ] );
- register_deactivation_hook( __FILE__, [ $this, 'deactivate' ] );
-
- add_action( 'before_woocommerce_init', [ $this, 'declare_woocommerce_feature_compatibility' ] );
- add_action( 'woocommerce_loaded', [ $this, 'init_plugin' ] );
- add_action( 'woocommerce_flush_rewrite_rules', [ $this, 'flush_rewrite_rules' ] );
-
- // Register admin notices to container and load notices
- $this->container['admin_notices'] = new \WeDevs\Dokan\Admin\Notices\Manager();
-
- $this->init_appsero_tracker();
-
- add_action( 'plugins_loaded', [ $this, 'woocommerce_not_loaded' ], 11 );
- }
-
- /**
- * Initializes the WeDevs_Dokan() class
- *
- * Checks for an existing WeDevs_WeDevs_Dokan() instance
- * and if it doesn't find one, create it.
- */
- public static function init() {
- if ( self::$instance === null ) {
- self::$instance = new self();
- }
-
- return self::$instance;
- }
-
- /**
- * Magic getter to bypass referencing objects
- *
- * @since 2.6.10
- *
- * @param string $prop
- *
- * @return object Class Instance
- */
- public function __get( $prop ) {
- if ( array_key_exists( $prop, $this->container ) ) {
- return $this->container[ $prop ];
- }
- }
-
- /**
- * Check if the PHP version is supported
- *
- * @return bool
- */
- public function is_supported_php() {
- if ( version_compare( PHP_VERSION, $this->min_php, '<=' ) ) {
- return false;
- }
-
- return true;
- }
-
- /**
- * Get the plugin path.
- *
- * @return string
- */
- public function plugin_path() {
- return untrailingslashit( plugin_dir_path( __FILE__ ) );
- }
-
- /**
- * Get the template path.
- *
- * @return string
- */
- public function template_path() {
- return apply_filters( 'dokan_template_path', 'dokan/' );
- }
-
- /**
- * Placeholder for activation function
- *
- * Nothing being called here yet.
- */
- public function activate() {
- if ( ! $this->has_woocommerce() ) {
- set_transient( 'dokan_wc_missing_notice', true );
- }
-
- if ( ! $this->is_supported_php() ) {
- require_once WC_ABSPATH . 'includes/wc-notice-functions.php';
-
- /* translators: 1: Required PHP Version 2: Running php version */
- wc_print_notice( sprintf( __( 'The Minimum PHP Version Requirement for Dokan is %1$s. You are Running PHP %2$s', 'dokan-lite' ), $this->min_php, phpversion() ), 'error' );
- exit;
- }
-
- require_once __DIR__ . '/includes/functions.php';
- require_once __DIR__ . '/includes/functions-compatibility.php';
-
- $this->container['upgrades'] = new \WeDevs\Dokan\Upgrade\Manager();
- $installer = new \WeDevs\Dokan\Install\Installer();
- $installer->do_install();
-
- // rewrite rules during dokan activation
- if ( $this->has_woocommerce() ) {
- $this->flush_rewrite_rules();
- }
- }
-
- /**
- * Flush rewrite rules after dokan is activated or woocommerce is activated
- *
- * @since 3.2.8
- */
- public function flush_rewrite_rules() {
- // fix rewrite rules
- if ( ! isset( $this->container['rewrite'] ) ) {
- $this->container['rewrite'] = new \WeDevs\Dokan\Rewrites();
- }
- $this->container['rewrite']->register_rule();
- flush_rewrite_rules();
- }
-
- /**
- * Placeholder for deactivation function
- *
- * Nothing being called here yet.
- */
- public function deactivate() {
- delete_transient( 'dokan_wc_missing_notice', true );
- }
-
- /**
- * Initialize plugin for localization
- *
- * @uses load_plugin_textdomain()
- */
- public function localization_setup() {
- load_plugin_textdomain( 'dokan-lite', false, dirname( plugin_basename( __FILE__ ) ) . '/languages/' );
- }
-
- /**
- * Define all constants
- *
- * @return void
- */
- public function define_constants() {
- $this->define( 'DOKAN_PLUGIN_VERSION', $this->version );
- $this->define( 'DOKAN_FILE', __FILE__ );
- $this->define( 'DOKAN_DIR', __DIR__ );
- $this->define( 'DOKAN_INC_DIR', __DIR__ . '/includes' );
- $this->define( 'DOKAN_LIB_DIR', __DIR__ . '/lib' );
- $this->define( 'DOKAN_PLUGIN_ASSEST', plugins_url( 'assets', __FILE__ ) );
-
- // give a way to turn off loading styles and scripts from parent theme
- $this->define( 'DOKAN_LOAD_STYLE', true );
- $this->define( 'DOKAN_LOAD_SCRIPTS', true );
- }
-
- /**
- * Define constant if not already defined
- *
- * @since 2.9.16
- *
- * @param string $name
- * @param string|bool $value
- *
- * @return void
- */
- private function define( $name, $value ) {
- if ( ! defined( $name ) ) {
- define( $name, $value );
- }
- }
+require_once __DIR__ . '/vendor/autoload.php';
+// Load files for loading the WeDevs_Dokan class.
+require_once __DIR__ . '/dokan-class.php';
- /**
- * Add High Performance Order Storage Support
- *
- * @since 3.8.0
- *
- * @return void
- */
- public function declare_woocommerce_feature_compatibility() {
- if ( class_exists( \Automattic\WooCommerce\Utilities\FeaturesUtil::class ) ) {
- \Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility( 'custom_order_tables', __FILE__, true );
- \Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility( 'cart_checkout_blocks', __FILE__, true );
- }
- }
+// Define constant for the Plugin file.
+defined( 'DOKAN_FILE' ) || define( 'DOKAN_FILE', __FILE__ );
- /**
- * Load the plugin after WP User Frontend is loaded
- *
- * @return void
- */
- public function init_plugin() {
- $this->includes();
- $this->init_hooks();
+// Use the necessary namespace.
+use WeDevs\Dokan\DependencyManagement\Container;
- do_action( 'dokan_loaded' );
- }
+// Declare the $dokan_container as global to access from the inside of the function.
+global $dokan_container;
- /**
- * Initialize the actions
- *
- * @return void
- */
- public function init_hooks() {
- // Localize our plugin
- add_action( 'init', [ $this, 'localization_setup' ] );
+// Instantiate the container.
+$dokan_container = new Container();
- // initialize the classes
- add_action( 'init', [ $this, 'init_classes' ], 4 );
- add_action( 'init', [ $this, 'wpdb_table_shortcuts' ], 1 );
+// Register the service providers.
+$dokan_container->addServiceProvider( new \WeDevs\Dokan\DependencyManagement\Providers\ServiceProvider() );
- add_action( 'plugins_loaded', [ $this, 'after_plugins_loaded' ] );
-
- add_filter( 'plugin_action_links_' . plugin_basename( __FILE__ ), [ $this, 'plugin_action_links' ] );
- add_action( 'in_plugin_update_message-dokan-lite/dokan.php', [ \WeDevs\Dokan\Install\Installer::class, 'in_plugin_update_message' ] );
-
- add_action( 'widgets_init', [ $this, 'register_widgets' ] );
- }
-
- /**
- * Include all the required files
- *
- * @return void
- */
- public function includes() {
- require_once DOKAN_DIR . '/deprecated/deprecated-functions.php';
- require_once DOKAN_DIR . '/deprecated/deprecated-hooks.php';
- require_once DOKAN_INC_DIR . '/functions.php';
-
- if ( ! function_exists( 'dokan_pro' ) ) {
- require_once DOKAN_INC_DIR . '/reports.php';
- }
-
- require_once DOKAN_INC_DIR . '/Order/functions.php';
- require_once DOKAN_INC_DIR . '/Product/functions.php';
- require_once DOKAN_INC_DIR . '/Withdraw/functions.php';
- require_once DOKAN_INC_DIR . '/functions-compatibility.php';
- require_once DOKAN_INC_DIR . '/wc-functions.php';
-
- require_once DOKAN_INC_DIR . '/wc-template.php';
- require_once DOKAN_DIR . '/deprecated/deprecated-classes.php';
-
- if ( is_admin() ) {
- require_once DOKAN_INC_DIR . '/Admin/functions.php';
- } else {
- require_once DOKAN_INC_DIR . '/template-tags.php';
- }
-
- require_once DOKAN_INC_DIR . '/store-functions.php';
- }
-
- /**
- * Init all the classes
- *
- * @return void
- */
- public function init_classes() {
- new \WeDevs\Dokan\Withdraw\Hooks();
- new \WeDevs\Dokan\Product\Hooks();
- new \WeDevs\Dokan\ProductCategory\Hooks();
- new \WeDevs\Dokan\Vendor\Hooks();
- new \WeDevs\Dokan\Upgrade\Hooks();
- new \WeDevs\Dokan\Vendor\UserSwitch();
- new \WeDevs\Dokan\CacheInvalidate();
- new \WeDevs\Dokan\Shipping\Hooks();
-
- if ( is_admin() ) {
- new \WeDevs\Dokan\Admin\Hooks();
- new \WeDevs\Dokan\Admin\Menu();
- new \WeDevs\Dokan\Admin\AdminBar();
- new \WeDevs\Dokan\Admin\Pointers();
- new \WeDevs\Dokan\Admin\Settings();
- new \WeDevs\Dokan\Admin\UserProfile();
- new \WeDevs\Dokan\Admin\SetupWizard();
- } else {
- new \WeDevs\Dokan\Vendor\StoreListsFilter();
- new \WeDevs\Dokan\ThemeSupport\Manager();
- }
-
- $this->container['product_block'] = new \WeDevs\Dokan\Blocks\ProductBlock();
- $this->container['pageview'] = new \WeDevs\Dokan\PageViews();
- $this->container['seller_wizard'] = new \WeDevs\Dokan\Vendor\SetupWizard();
- $this->container['core'] = new \WeDevs\Dokan\Core();
- $this->container['scripts'] = new \WeDevs\Dokan\Assets();
- $this->container['email'] = new \WeDevs\Dokan\Emails\Manager();
- $this->container['vendor'] = new \WeDevs\Dokan\Vendor\Manager();
- $this->container['product'] = new \WeDevs\Dokan\Product\Manager();
- $this->container['shortcodes'] = new \WeDevs\Dokan\Shortcodes\Shortcodes();
- $this->container['registration'] = new \WeDevs\Dokan\Registration();
- $this->container['order'] = new \WeDevs\Dokan\Order\Manager();
- $this->container['order_controller'] = new \WeDevs\Dokan\Order\Controller();
- $this->container['api'] = new \WeDevs\Dokan\REST\Manager();
- $this->container['withdraw'] = new \WeDevs\Dokan\Withdraw\Manager();
- $this->container['dashboard'] = new \WeDevs\Dokan\Dashboard\Manager();
- $this->container['commission'] = new \WeDevs\Dokan\Commission();
- $this->container['customizer'] = new \WeDevs\Dokan\Customizer();
- $this->container['upgrades'] = new \WeDevs\Dokan\Upgrade\Manager();
- $this->container['product_sections'] = new \WeDevs\Dokan\ProductSections\Manager();
- $this->container['reverse_withdrawal'] = new \WeDevs\Dokan\ReverseWithdrawal\ReverseWithdrawal();
- $this->container['dummy_data_importer'] = new \WeDevs\Dokan\DummyData\Importer();
- $this->container['catalog_mode'] = new \WeDevs\Dokan\CatalogMode\Controller();
- $this->container['bg_process'] = new \WeDevs\Dokan\BackgroundProcess\Manager();
- $this->container['frontend_manager'] = new \WeDevs\Dokan\Frontend\Frontend();
-
- //fix rewrite rules
- if ( ! isset( $this->container['rewrite'] ) ) {
- $this->container['rewrite'] = new \WeDevs\Dokan\Rewrites();
- }
-
- $this->container = apply_filters( 'dokan_get_class_container', $this->container );
-
- if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) {
- new \WeDevs\Dokan\Ajax();
- }
-
- new \WeDevs\Dokan\Privacy();
- }
-
- /**
- * Load table prefix for withdraw and orders table
- *
- * @since 1.0
- *
- * @return void
- */
- public function wpdb_table_shortcuts() {
- global $wpdb;
-
- $wpdb->dokan_withdraw = $wpdb->prefix . 'dokan_withdraw';
- $wpdb->dokan_orders = $wpdb->prefix . 'dokan_orders';
- $wpdb->dokan_announcement = $wpdb->prefix . 'dokan_announcement';
- $wpdb->dokan_refund = $wpdb->prefix . 'dokan_refund';
- $wpdb->dokan_vendor_balance = $wpdb->prefix . 'dokan_vendor_balance';
- }
-
- /**
- * Executed after all plugins are loaded
- *
- * At this point Dokan Pro is loaded
- *
- * @since 2.8.7
- *
- * @return void
- */
- public function after_plugins_loaded() {
- // Initiate background processes
- $processes = get_option( 'dokan_background_processes', [] );
-
- if ( ! empty( $processes ) ) {
- $update = false;
- foreach ( $processes as $processor => $file ) {
- if ( file_exists( $file ) ) {
- include_once $file;
- new $processor();
- } else {
- $update = true;
- unset( $processes[ $processor ] );
- }
- }
- if ( $update ) {
- update_option( 'dokan_background_processes', $processes );
- }
- }
- }
-
- /**
- * Register widgets
- *
- * @since 2.8
- *
- * @return void
- */
- public function register_widgets() {
- $this->container['widgets'] = new \WeDevs\Dokan\Widgets\Manager();
- }
-
- /**
- * Returns if the plugin is in PRO version
- *
- * @since 2.4
- *
- * @return bool
- */
- public function is_pro_exists() {
- return apply_filters( 'dokan_is_pro_exists', false );
- }
-
- /**
- * Plugin action links
- *
- * @param array $links
- *
- * @since 2.4
- *
- * @return array
- */
- public function plugin_action_links( $links ) {
- if ( ! $this->is_pro_exists() ) {
- $links[] = '' . __( 'Get Pro', 'dokan-lite' ) . '';
- }
-
- $links[] = '' . __( 'Settings', 'dokan-lite' ) . '';
- $links[] = '' . __( 'Documentation', 'dokan-lite' ) . '';
-
- return $links;
- }
-
- /**
- * Initialize Appsero Tracker
- *
- * @return void
- */
- public function init_appsero_tracker() {
- $this->container['tracker'] = new \WeDevs\Dokan\Tracker();
- }
-
- /**
- * Check whether woocommerce is installed and active
- *
- * @since 2.9.16
- *
- * @return bool
- */
- public function has_woocommerce() {
- return class_exists( 'WooCommerce' );
- }
-
- /**
- * Check whether woocommerce is installed
- *
- * @since 3.2.8
- *
- * @return bool
- */
- public function is_woocommerce_installed() {
- return in_array( 'woocommerce/woocommerce.php', array_keys( get_plugins() ), true );
- }
-
- /**
- * Handles scenerios when WooCommerce is not active
- *
- * @since 2.9.27
- *
- * @return void
- */
- public function woocommerce_not_loaded() {
- if ( did_action( 'woocommerce_loaded' ) || ! is_admin() ) {
- return;
- }
-
- require_once DOKAN_INC_DIR . '/functions.php';
-
- if ( get_transient( '_dokan_setup_page_redirect' ) ) {
- dokan_redirect_to_admin_setup_wizard();
- }
-
- new \WeDevs\Dokan\Admin\SetupWizardNoWC();
- }
+/**
+ * Get the container.
+ *
+ * @return Container The global container instance.
+ */
+function dokan_get_container(): Container {
+ global $dokan_container;
- /**
- * Get Dokan db version key
- *
- * @since 3.0.0
- *
- * @return string
- */
- public function get_db_version_key() {
- return $this->db_version_key;
- }
+ return $dokan_container;
}
/**
- * Load Dokan Plugin when all plugins loaded
+ * Load Dokan Plugin when all plugins loaded.
*
- * @return WeDevs_Dokan
+ * @return WeDevs_Dokan The singleton instance of WeDevs_Dokan.
*/
-function dokan() { // phpcs:ignore
+function dokan() {
return WeDevs_Dokan::init();
}
-// Lets Go....
+// Let's go...
dokan();
diff --git a/includes/Analytics/Reports/BaseQueryFilter.php b/includes/Analytics/Reports/BaseQueryFilter.php
new file mode 100644
index 0000000000..dbb7de1292
--- /dev/null
+++ b/includes/Analytics/Reports/BaseQueryFilter.php
@@ -0,0 +1,179 @@
+register_hooks();
+ }
+
+ /**
+ * Add join clause for Dokan order state table in WooCommerce analytics queries.
+ *
+ * @param array $clauses The existing join clauses.
+ *
+ * @return array The modified join clauses.
+ */
+ public function add_join_subquery( array $clauses ): array {
+ global $wpdb;
+
+ $dokan_order_state_table = $this->get_dokan_table();
+
+ $clauses[] = "JOIN {$dokan_order_state_table} ON {$wpdb->prefix}{$this->wc_table}.order_id = {$dokan_order_state_table}.order_id";
+
+ return array_unique( $clauses );
+ }
+
+ /**
+ * Add where clause for Dokan order state in WooCommerce analytics queries.
+ *
+ * @param array $clauses The existing where clauses.
+ *
+ * @return array The modified where clauses.
+ */
+ public function add_where_subquery( array $clauses ): array {
+ $dokan_order_state_table = $this->get_dokan_table();
+ $order_types = $this->get_order_and_refund_types_to_include();
+
+ $clauses[] = "AND {$dokan_order_state_table}.order_type in ( $order_types ) ";
+
+ $clauses = $this->add_where_subquery_for_refund( $clauses );
+ $clauses = $this->add_where_subquery_for_seller_filter( $clauses );
+
+ return array_unique( $clauses );
+ }
+
+ /**
+ * Add where clause for refunds in WooCommerce analytics queries.
+ *
+ * @param array $clauses The existing where clauses.
+ *
+ * @return array The modified where clauses.
+ */
+ protected function add_where_subquery_for_refund( array $clauses ): array {
+ if ( ! isset( $_GET['refunds'] ) ) {
+ return $clauses;
+ }
+
+ $dokan_order_state_table = $this->get_dokan_table();
+ $order_types = $this->get_refund_types_to_include();
+
+ $clauses[] = "AND {$dokan_order_state_table}.order_type in ( $order_types ) ";
+
+ return $clauses;
+ }
+
+ /**
+ * Determine if the query should be filtered by seller ID.
+ *
+ * @return bool True if the query should be filtered by seller ID, false otherwise.
+ */
+ public function should_filter_by_seller_id(): bool {
+ return true;
+ }
+
+ /**
+ * Get the order types to include in WooCommerce analytics queries.
+ *
+ * @return string The order types to include.
+ */
+ protected function get_order_and_refund_types_to_include(): string {
+ $order_type = new OrderType();
+
+ if ( $this->should_filter_by_seller_id() ) {
+ return implode( ',', $order_type->get_seller_order_types() );
+ }
+
+ return implode( ',', $order_type->get_admin_order_types() );
+ }
+
+ /**
+ * Get the refund types to include in WooCommerce analytics queries.
+ *
+ * @return string The refund types to include.
+ */
+ protected function get_refund_types_to_include(): string {
+ $order_type = new OrderType();
+
+ if ( $this->should_filter_by_seller_id() ) {
+ return implode( ',', $order_type->get_seller_refund_types() );
+ }
+
+ return implode( ',', $order_type->get_admin_refund_types() );
+ }
+
+ protected function get_dokan_table(): string {
+ return DataStore::get_db_table_name();
+ }
+
+ /**
+ * Get the non refund order types to include in WooCommerce analytics queries.
+ *
+ * @return string The refund types to include.
+ */
+ protected function get_non_refund_order_types_to_include(): string {
+ $order_type = new OrderType();
+
+ if ( $this->should_filter_by_seller_id() ) {
+ return implode( ',', $order_type->get_seller_non_refund_order_types() );
+ }
+
+ return implode( ',', $order_type->get_admin_non_refund_order_types() );
+ }
+
+ /**
+ * Add where clause for seller query filter in WooCommerce analytics queries.
+ *
+ * @param array $clauses The existing where clauses.
+ *
+ * @return array The modified where clauses.
+ */
+ protected function add_where_subquery_for_seller_filter( array $clauses ): array {
+ $seller_id = $this->get_seller_id();
+
+ if ( ! $seller_id ) {
+ return $clauses;
+ }
+
+ $dokan_order_state_table = $this->get_dokan_table();
+
+ global $wpdb;
+
+ $clauses[] = $wpdb->prepare( "AND {$dokan_order_state_table}.seller_id = %s", $seller_id ); //phpcs:ignore
+
+ return $clauses;
+ }
+
+ /**
+ * Get seller id from Query param for Admin and currently logged in user as Vendor
+ *
+ * @return int
+ */
+ public function get_seller_id() {
+ if ( ! is_user_logged_in() ) {
+ return 0;
+ }
+
+ if ( ! current_user_can( 'manage_options' ) ) {
+ return dokan_get_current_user_id();
+ }
+
+ return (int) ( wp_unslash( $_GET['sellers'] ?? 0 ) ); // phpcs:ignore
+ }
+}
diff --git a/includes/Analytics/Reports/Categories/QueryFilter.php b/includes/Analytics/Reports/Categories/QueryFilter.php
new file mode 100644
index 0000000000..d5428315b9
--- /dev/null
+++ b/includes/Analytics/Reports/Categories/QueryFilter.php
@@ -0,0 +1,33 @@
+context ) {
+ return $column;
+ }
+
+ $order_type = new OrderType();
+ $seller_types = implode( ',', $order_type->get_seller_order_types() );
+ $admin_types = implode( ',', $order_type->get_admin_order_types() );
+ $table_name = $this->get_dokan_table();
+
+ /**
+ * Parent order ID need to be set to 0 for Dokan suborder.
+ * Because, WC orders analytics generates order details link against the parent order
+ * assuming the order having parent ID as a refund order.
+ */
+ // $column['parent_id'] = "(CASE WHEN {$table_name}.order_type NOT IN ($refund_types) THEN 0 ELSE {$wc_table_name}.parent_id END ) as parent_id";
+ $column['amount'] = "SUM(CASE WHEN {$table_name}.order_type IN ($admin_types) THEN discount_amount ELSE 0 END ) as amount";
+ $column['orders_count'] = "COUNT( DISTINCT (CASE WHEN {$table_name}.order_type IN ($seller_types) THEN {$table_name}.order_id END) ) as orders_count";
+
+ return $column;
+ }
+}
diff --git a/includes/Analytics/Reports/Coupons/Stats/QueryFilter.php b/includes/Analytics/Reports/Coupons/Stats/QueryFilter.php
new file mode 100644
index 0000000000..9c1e33c560
--- /dev/null
+++ b/includes/Analytics/Reports/Coupons/Stats/QueryFilter.php
@@ -0,0 +1,40 @@
+clear_all_clauses();
+ unset( $this->subquery );
+ $this->total_query = new WcSqlQuery( $this->context . '_total' );
+ $this->total_query->add_sql_clause( 'from', self::get_db_table_name() );
+
+ $this->interval_query = new WcSqlQuery( $this->context . '_interval' );
+ $this->interval_query->add_sql_clause( 'from', self::get_db_table_name() );
+ $this->interval_query->add_sql_clause( 'group_by', 'time_interval' );
+ }
+}
diff --git a/includes/Analytics/Reports/Customers/QueryFilter.php b/includes/Analytics/Reports/Customers/QueryFilter.php
new file mode 100644
index 0000000000..7fcdf72694
--- /dev/null
+++ b/includes/Analytics/Reports/Customers/QueryFilter.php
@@ -0,0 +1,65 @@
+context ) {
+ return $column;
+ }
+
+ $types = $this->get_order_and_refund_types_to_include();
+ $table_name = $this->get_dokan_table();
+
+ $orders_count = "COUNT( DISTINCT (CASE WHEN {$table_name}.order_type IN ($types) THEN {$table_name}.order_id END) ) "; //'SUM( CASE WHEN parent_id = 0 THEN 1 ELSE 0 END )';
+ $total_spend = "SUM(CASE WHEN {$table_name}.order_type IN ($types) THEN total_sales ELSE 0 END )"; //'SUM( total_sales )';
+
+ /**
+ * Parent order ID need to be set to 0 for Dokan suborder.
+ * Because, WC orders analytics generates order details link against the parent order
+ * assuming the order having parent ID as a refund order.
+ */
+ $column['orders_count'] = "{$orders_count} as orders_count";
+ $column['total_spend'] = "{$total_spend} as total_spend";
+ $column['avg_order_value'] = "CASE WHEN {$orders_count} = 0 THEN NULL ELSE {$total_spend} / {$orders_count} END AS avg_order_value";
+
+ return $column;
+ }
+}
diff --git a/includes/Analytics/Reports/Customers/Stats/QueryFilter.php b/includes/Analytics/Reports/Customers/Stats/QueryFilter.php
new file mode 100644
index 0000000000..3a10e7b1ca
--- /dev/null
+++ b/includes/Analytics/Reports/Customers/Stats/QueryFilter.php
@@ -0,0 +1,81 @@
+modify_select_field( $clause );
+ }
+
+ return $modified_clauses;
+ }
+
+ protected function modify_select_field( string $field ): string {
+ $parts = explode( ' as ', strtolower( $field ) );
+ $renamed_field = trim( $parts[1] ?? '' );
+
+ // The following fields need be modified.
+ // [0] => SUM( total_sales ) AS total_spend,
+ // [1] => SUM( CASE WHEN parent_id = 0 THEN 1 END ) as orders_count,
+ // [2] => CASE WHEN SUM( CASE WHEN parent_id = 0 THEN 1 ELSE 0 END ) = 0 THEN NULL ELSE SUM( total_sales ) / SUM( CASE WHEN parent_id = 0 THEN 1 ELSE 0 END ) END AS avg_order_value
+
+ $table_name = $this->get_dokan_table();
+
+ $types = $this->get_order_and_refund_types_to_include();
+
+ switch ( str_replace( ',', '', $renamed_field ) ) {
+ case 'total_spend':
+ $field = "SUM(CASE WHEN {$table_name}.order_type IN ($types) THEN total_sales ELSE 0 END ) as total_spend";
+ break;
+ case 'orders_count':
+ $field = "COUNT( DISTINCT (CASE WHEN {$table_name}.order_type IN ($types) THEN {$table_name}.order_id END) ) as orders_count";
+ break;
+ case 'avg_order_value':
+ $order_types_conditions = "CASE WHEN {$table_name}.order_type IN ($types) THEN 1 ELSE 0 END";
+
+ $field = "CASE WHEN SUM( $order_types_conditions ) = 0 THEN NULL ELSE SUM( total_sales ) / SUM( $order_types_conditions ) END AS avg_order_value";
+ break;
+ default:
+ break;
+ }
+
+ // Append comma if the given field ends with comma[","].
+ if ( str_ends_with( $renamed_field, ',' ) ) {
+ $field = $field . ',';
+ }
+
+ return $field;
+ }
+}
diff --git a/includes/Analytics/Reports/DataStoreModifier.php b/includes/Analytics/Reports/DataStoreModifier.php
new file mode 100644
index 0000000000..2048614054
--- /dev/null
+++ b/includes/Analytics/Reports/DataStoreModifier.php
@@ -0,0 +1,58 @@
+register_hooks();
+ }
+
+ public function register_hooks(): void {
+ add_filter( 'woocommerce_data_stores', [ $this, 'modify_wc_products_stats_datastore' ], 20 );
+ }
+
+ /**
+ * Customize the WooCommerce products stats datastore to override the $total_query and $interval_query properties.
+ * This modification replaces the Automattic\WooCommerce\Admin\API\Reports\SqlQuery class with WeDevs\Dokan\Analytics\Reports\WcSqlQuery
+ * to apply specific filters to queries.
+ * The reason for this change is that the "get_sql_clause" method's second parameter defaults to "unfiltered," which blocks the filters we need
+ * to add JOIN and WHERE clauses for the dokan_order_stats table.
+ *
+ * @see https://github.com/woocommerce/woocommerce/blob/9297409c5a705d1cd0ae65ec9b058271bd90851e/plugins/woocommerce/src/Admin/API/Reports/Products/Stats/DataStore.php#L170
+ *
+ * @param array $wc_stores An array of WooCommerce datastores.
+ * @return array Modified array of WooCommerce datastores.
+ */
+ public function modify_wc_products_stats_datastore( $wc_stores ) {
+ if ( isset( $wc_stores['report-products-stats'] ) ) {
+ $wc_stores['report-products-stats'] = \WeDevs\Dokan\Analytics\Reports\Products\Stats\WcDataStore::class;
+ }
+
+ if ( isset( $wc_stores['report-taxes-stats'] ) ) {
+ $wc_stores['report-taxes-stats'] = \WeDevs\Dokan\Analytics\Reports\Taxes\Stats\WcDataStore::class;
+ }
+
+ if ( isset( $wc_stores['report-orders-stats'] ) ) {
+ $wc_stores['report-orders-stats'] = \WeDevs\Dokan\Analytics\Reports\Orders\Stats\WcDataStore::class;
+ }
+
+ if ( isset( $wc_stores['report-coupons-stats'] ) ) {
+ $wc_stores['report-coupons-stats'] = \WeDevs\Dokan\Analytics\Reports\Coupons\Stats\WcDataStore::class;
+ }
+
+ if ( isset( $wc_stores['report-stock-stats'] ) ) {
+ $wc_stores['report-stock-stats'] = \WeDevs\Dokan\Analytics\Reports\Stock\Stats\WcDataStore::class;
+ }
+
+ return $wc_stores;
+ }
+}
diff --git a/includes/Analytics/Reports/OrderType.php b/includes/Analytics/Reports/OrderType.php
new file mode 100644
index 0000000000..fbc359ae91
--- /dev/null
+++ b/includes/Analytics/Reports/OrderType.php
@@ -0,0 +1,193 @@
+get_parent_id() ) {
+ return false;
+ }
+
+ if ( $order instanceof \WC_Order ) {
+ return true;
+ }
+
+ $parent_order = wc_get_order( $order->get_parent_id() );
+
+ return $this->is_dokan_suborder_related( $parent_order );
+ }
+
+ /**
+ * Determines the type of the given order based on its relation to Dokan suborders and refunds.
+ *
+ * @param \WC_Abstract_Order $order The order object to classify.
+ *
+ * @return int The order type constant.
+ */
+ public function get_type( \WC_Abstract_Order $order ): int {
+ $is_suborder_related = $this->is_dokan_suborder_related( $order );
+
+ if ( $is_suborder_related ) {
+ // Refund of Dokan suborder.
+ if ( $order instanceof WC_Order_Refund ) {
+ return self::DOKAN_SUBORDER_REFUND;
+ }
+
+ // Dokan Suborder
+ return self::DOKAN_SUBORDER;
+ }
+
+ if ( ! $is_suborder_related ) {
+ // Refund of WC order.
+ if ( $order instanceof WC_Order_Refund ) {
+ $suborder_ids = array_filter(
+ (array) dokan_get_suborder_ids_by( $order->get_parent_id() )
+ );
+
+ if ( count( $suborder_ids ) ) {
+ return self::DOKAN_PARENT_ORDER_REFUND;
+ }
+
+ return self::DOKAN_SINGLE_ORDER_REFUND;
+ }
+
+ $suborder_ids = dokan_get_suborder_ids_by( $order->get_id() );
+
+ // Dokan Single Vendor Order
+ if ( $suborder_ids === null || ( is_array( $suborder_ids ) && count( $suborder_ids ) === 0 ) ) {
+ return self::DOKAN_SINGLE_ORDER;
+ }
+ }
+
+ return self::DOKAN_PARENT_ORDER;
+ }
+
+ /**
+ * Gets the list of order types relevant to admin users.
+ *
+ * @return array List of admin order type constants.
+ */
+ public function get_admin_order_types(): array {
+ return [
+ self::DOKAN_PARENT_ORDER,
+ self::DOKAN_SINGLE_ORDER,
+ self::DOKAN_PARENT_ORDER_REFUND,
+ self::DOKAN_SINGLE_ORDER_REFUND,
+ ];
+ }
+
+ /**
+ * Gets the list of order types relevant to sellers.
+ *
+ * @return array List of seller order type constants.
+ */
+ public function get_seller_order_types(): array {
+ return [
+ self::DOKAN_SINGLE_ORDER,
+ self::DOKAN_SUBORDER,
+ self::DOKAN_SUBORDER_REFUND,
+ self::DOKAN_SINGLE_ORDER_REFUND,
+ ];
+ }
+
+ /**
+ * Gets the list of order types (excluding refunds) relevant to admin users.
+ *
+ * @return array List of admin order type constants (non-refund).
+ */
+ public function get_admin_non_refund_order_types(): array {
+ return [
+ self::DOKAN_PARENT_ORDER,
+ self::DOKAN_SINGLE_ORDER,
+ ];
+ }
+
+ /**
+ * Gets the list of order types (excluding refunds) relevant to sellers.
+ *
+ * @return array List of seller order type constants (non-refund).
+ */
+ public function get_seller_non_refund_order_types(): array {
+ return [
+ self::DOKAN_SINGLE_ORDER,
+ self::DOKAN_SUBORDER,
+ ];
+ }
+
+ /**
+ * Gets the list of refund types relevant to all users.
+ *
+ * @return array List of refund type constants.
+ */
+ public function get_refund_types(): array {
+ return [
+ self::DOKAN_PARENT_ORDER_REFUND,
+ self::DOKAN_SUBORDER_REFUND,
+ self::DOKAN_SINGLE_ORDER_REFUND,
+ ];
+ }
+
+ /**
+ * Gets the list of refund types relevant to sellers.
+ *
+ * @return array List of seller refund type constants.
+ */
+ public function get_seller_refund_types(): array {
+ return [
+ self::DOKAN_SUBORDER_REFUND,
+ self::DOKAN_SINGLE_ORDER_REFUND,
+ ];
+ }
+
+ /**
+ * Gets the list of refund types relevant to admin users.
+ *
+ * @return array List of admin refund type constants.
+ */
+ public function get_admin_refund_types(): array {
+ return [
+ self::DOKAN_PARENT_ORDER_REFUND,
+ self::DOKAN_SINGLE_ORDER_REFUND,
+ ];
+ }
+
+ /**
+ * Gets the list of refund types relevant to admin users.
+ *
+ * @return array List of admin refund type constants.
+ */
+ public function get_all_order_types(): array {
+ return [
+ self::DOKAN_PARENT_ORDER,
+ self::DOKAN_SINGLE_ORDER,
+ self::DOKAN_SUBORDER,
+ self::DOKAN_PARENT_ORDER_REFUND,
+ self::DOKAN_SUBORDER_REFUND,
+ self::DOKAN_SINGLE_ORDER_REFUND,
+ ];
+ }
+}
diff --git a/includes/Analytics/Reports/Orders/QueryFilter.php b/includes/Analytics/Reports/Orders/QueryFilter.php
new file mode 100644
index 0000000000..922c1a8c8e
--- /dev/null
+++ b/includes/Analytics/Reports/Orders/QueryFilter.php
@@ -0,0 +1,91 @@
+get_refund_types() );
+ $table_name = Stats\DataStore::get_db_table_name();
+
+ /**
+ * Parent order ID need to be set to 0 for Dokan suborder.
+ * Because, WC orders analytics generates order details link against the parent order
+ * assuming the order having parent ID as a refund order.
+ */
+ $column['parent_id'] = "(CASE WHEN {$table_name}.order_type NOT IN ($refund_types) THEN 0 ELSE {$wc_table_name}.parent_id END ) as parent_id";
+
+ return $column;
+ }
+
+ /**
+ * Exclude order IDs from WooCommerce analytics queries based on seller or admin context.
+ *
+ * @param array $ids The existing excluded order IDs.
+ * @param array $query_args The query arguments.
+ * @param string $field The field being queried.
+ * @param string $context The context of the query.
+ *
+ * @return array The modified excluded order IDs.
+ */
+ public function exclude_order_ids( array $ids, array $query_args, string $field, $context ): array {
+ if ( $context !== 'orders' || ! $this->should_filter_by_seller_id() ) {
+ return $ids;
+ }
+
+ return [];
+ }
+
+ /**
+ * Add custom columns to the select clause of WooCommerce analytics queries.
+ *
+ * @param array $clauses The existing select clauses.
+ *
+ * @return array The modified select clauses.
+ */
+ public function add_select_subquery( array $clauses ): array {
+ $clauses[] = ', seller_earning, seller_gateway_fee, seller_discount, admin_commission, admin_gateway_fee, admin_discount, admin_subsidy';
+
+ return array_unique( $clauses );
+ }
+}
diff --git a/includes/Analytics/Reports/Orders/Stats/DataStore.php b/includes/Analytics/Reports/Orders/Stats/DataStore.php
new file mode 100644
index 0000000000..0048d96d6a
--- /dev/null
+++ b/includes/Analytics/Reports/Orders/Stats/DataStore.php
@@ -0,0 +1,215 @@
+date_column_name = get_option( 'woocommerce_date_type', 'date_paid' );
+ parent::__construct();
+ }
+
+ /**
+ * Get the data based on args.
+ *
+ * @param array $args Query parameters.
+ * @return stdClass|WP_Error
+ */
+ public function get_data( $args ) {
+ throw new Exception( 'Not supported by Dokan' );
+ }
+
+ /**
+ * Add order information to the lookup table when orders are created or modified.
+ *
+ * @param int $post_id Post ID.
+ * @return int|bool Returns -1 if order won't be processed, or a boolean indicating processing success.
+ */
+ public static function sync_order( $post_id ) {
+ if ( ! OrderUtil::is_order( $post_id, array( 'shop_order', 'shop_order_refund' ) ) ) {
+ return -1;
+ }
+
+ $order = wc_get_order( $post_id );
+ if ( ! $order ) {
+ return -1;
+ }
+
+ return self::update( $order );
+ }
+
+ /**
+ * Update the database with stats data.
+ *
+ * @param \WC_Order|\WC_Order_Refund $order Order or refund to update row for.
+ * @return int|bool Returns -1 if order won't be processed, or a boolean indicating processing success.
+ */
+ public static function update( $order ) {
+ global $wpdb;
+ $table_name = self::get_db_table_name();
+
+ if ( ! $order->get_id() || ! $order->get_date_created() ) {
+ return -1;
+ }
+
+ $vendor_earning = (float) dokan()->commission->get_earning_by_order( $order );
+ $admin_earning = (float) dokan()->commission->get_earning_by_order( $order, 'admin' );
+
+ $gateway_fee = $order->get_meta( 'dokan_gateway_fee' );
+ $gateway_fee_provider = $order->get_meta( 'dokan_gateway_fee_paid_by' );
+
+ $shipping_fee = $order->get_shipping_total();
+ $shipping_fee_recipient = $order->get_meta( 'shipping_fee_recipient' );
+
+ /**
+ * Filters order stats data.
+ *
+ * @param array $data Data written to order stats lookup table.
+ * @param WC_Order $order Order object.
+ *
+ * @since 4.0.0
+ */
+ $data = apply_filters(
+ 'dokan_analytics_update_order_stats_data',
+ array(
+ 'order_id' => $order->get_id(),
+ 'seller_id' => (int) $order->get_meta( '_dokan_vendor_id' ),
+ 'order_type' => (int) ( ( new OrderType() )->get_type( $order ) ),
+ // Seller Data
+ 'seller_earning' => $vendor_earning,
+ 'seller_gateway_fee' => $gateway_fee_provider === 'seller' ? $gateway_fee : '0',
+ 'seller_shipping_fee' => $shipping_fee_recipient === 'seller' ? $shipping_fee : '0',
+ 'seller_discount' => $order->get_meta( '_seller_discount' ),
+ // Admin Data
+ 'admin_commission' => $admin_earning,
+ 'admin_gateway_fee' => $gateway_fee_provider !== 'seller' ? $gateway_fee : '0',
+ 'admin_shipping_fee' => $shipping_fee_recipient !== 'seller' ? $shipping_fee : '0',
+ 'admin_discount' => $order->get_meta( '_admin_discount' ),
+ 'admin_subsidy' => $order->get_meta( '_admin_subsidy' ),
+ ),
+ $order,
+ );
+
+ $format = array(
+ '%d',
+ '%d',
+ '%d',
+ // Seller data
+ '%f',
+ '%f',
+ '%f',
+ '%f',
+ // Admin data
+ '%f',
+ '%f',
+ '%f',
+ '%f',
+ '%f',
+ );
+
+ // Update or add the information to the DB.
+ $result = $wpdb->replace( $table_name, $data, $format );
+
+ /**
+ * Fires when Dokan order's stats reports are updated.
+ *
+ * @param int $order_id Order ID.
+ *
+ * @since DOKAN_SINCE
+ */
+ do_action( 'dokan_analytics_update_order_stats', $order->get_id() );
+
+ // Check the rows affected for success. Using REPLACE can affect 2 rows if the row already exists.
+ return ( 1 === $result || 2 === $result );
+ }
+
+ /**
+ * Deletes the order stats when an order is deleted.
+ *
+ * @param int $post_id Post ID.
+ */
+ public static function delete_order( $post_id ) {
+ global $wpdb;
+ $order_id = (int) $post_id;
+
+ if ( ! OrderUtil::is_order( $post_id, array( 'shop_order', 'shop_order_refund' ) ) ) {
+ return;
+ }
+
+ // Retrieve customer details before the order is deleted.
+ $order = wc_get_order( $order_id );
+ $customer_id = absint( CustomersDataStore::get_existing_customer_id_from_order( $order ) );
+
+ // Delete the order.
+ $wpdb->delete( self::get_db_table_name(), array( 'order_id' => $order_id ) );
+
+ /**
+ * Fires when orders stats are deleted.
+ *
+ * @param int $order_id Order ID.
+ * @param int $customer_id Customer ID.
+ *
+ * @since 4.0.0
+ */
+ do_action( 'dokan_analytics_delete_order_stats', $order_id, $customer_id );
+
+ ReportsCache::invalidate();
+ }
+}
diff --git a/includes/Analytics/Reports/Orders/Stats/QueryFilter.php b/includes/Analytics/Reports/Orders/Stats/QueryFilter.php
new file mode 100644
index 0000000000..3d867f7a0e
--- /dev/null
+++ b/includes/Analytics/Reports/Orders/Stats/QueryFilter.php
@@ -0,0 +1,88 @@
+context ) {
+ return $column;
+ }
+
+ $table_name = $this->get_dokan_table();
+ $types = $this->get_non_refund_order_types_to_include();
+
+ $column['orders_count'] = "SUM( CASE WHEN {$table_name}.order_type IN ($types) THEN 1 ELSE 0 END ) as orders_count";
+ $column['avg_items_per_order'] = "SUM( {$wc_table_name}.num_items_sold ) / SUM( CASE WHEN {$table_name}.order_type IN($types) THEN 1 ELSE 0 END ) AS avg_items_per_order";
+ $column['avg_order_value'] = "SUM( {$wc_table_name}.net_total ) / SUM( CASE WHEN {$table_name}.order_type IN($types) THEN 1 ELSE 0 END ) AS avg_order_value";
+ $column['avg_admin_commission'] = "SUM( {$table_name}.admin_commission ) / SUM( CASE WHEN {$table_name}.order_type IN($types) THEN 1 ELSE 0 END ) AS avg_admin_commission";
+ $column['avg_seller_earning'] = "SUM( {$table_name}.seller_earning ) / SUM( CASE WHEN {$table_name}.order_type IN($types) THEN 1 ELSE 0 END ) AS avg_seller_earning";
+
+ return $column;
+ }
+
+ /**
+ * Adds custom select subqueries for calculating Dokan-specific totals in the analytics reports.
+ *
+ * @param array $clauses The existing SQL select clauses.
+ *
+ * @return array Modified SQL select clauses.
+ */
+ public function add_select_subquery_for_total( $clauses ) {
+ $table_name = $this->get_dokan_table();
+ $types = $this->get_order_and_refund_types_to_include();
+
+ $clauses[] = ', sum(seller_earning) as total_seller_earning, sum(seller_gateway_fee) as total_seller_gateway_fee, sum(seller_discount) as total_seller_discount, sum(admin_commission) as total_admin_commission, sum(admin_gateway_fee) as total_admin_gateway_fee, sum(admin_discount) as total_admin_discount, sum(admin_subsidy) as total_admin_subsidy';
+ $clauses[] = ", SUM( {$table_name}.admin_commission ) / SUM( CASE WHEN {$table_name}.order_type IN($types) THEN 1 ELSE 0 END ) AS avg_admin_commission";
+ $clauses[] = ", SUM( {$table_name}.seller_earning ) / SUM( CASE WHEN {$table_name}.order_type IN($types) THEN 1 ELSE 0 END ) AS avg_seller_earning";
+
+ return $clauses;
+ }
+}
diff --git a/includes/Analytics/Reports/Orders/Stats/ScheduleListener.php b/includes/Analytics/Reports/Orders/Stats/ScheduleListener.php
new file mode 100644
index 0000000000..31b7a58a6c
--- /dev/null
+++ b/includes/Analytics/Reports/Orders/Stats/ScheduleListener.php
@@ -0,0 +1,53 @@
+register_hooks();
+ }
+
+ /**
+ * Register hooks for WooCommerce analytics order events.
+ *
+ * @return void
+ */
+ public function register_hooks(): void {
+ add_action( 'woocommerce_analytics_update_order_stats', [ $this, 'sync_dokan_order' ] );
+ add_action( 'woocommerce_before_delete_order', [ $this, 'delete_order' ] );
+ add_action( 'delete_post', [ $this, 'delete_order' ] );
+ }
+
+ /**
+ * Sync Dokan order data when WooCommerce analytics updates order stats.
+ *
+ * @param int $order_id The ID of the order being updated.
+ *
+ * @return void
+ */
+ public function sync_dokan_order( $order_id ) {
+ return \WeDevs\Dokan\Analytics\Reports\Orders\Stats\DataStore::sync_order( $order_id );
+ }
+
+ /**
+ * Delete Dokan order data when WooCommerce deletes an order.
+ *
+ * @param int $order_id The ID of the order being deleted.
+ *
+ * @return void
+ */
+ public function delete_order( $order_id ) {
+ return \WeDevs\Dokan\Analytics\Reports\Orders\Stats\DataStore::delete_order( $order_id );
+ }
+}
diff --git a/includes/Analytics/Reports/Orders/Stats/WcDataStore.php b/includes/Analytics/Reports/Orders/Stats/WcDataStore.php
new file mode 100644
index 0000000000..a098f4264d
--- /dev/null
+++ b/includes/Analytics/Reports/Orders/Stats/WcDataStore.php
@@ -0,0 +1,28 @@
+clear_all_clauses();
+ unset( $this->subquery );
+ $this->total_query = new WcSqlQuery( $this->context . '_total' );
+ $this->total_query->add_sql_clause( 'from', self::get_db_table_name() );
+
+ $this->interval_query = new WcSqlQuery( $this->context . '_interval' );
+ $this->interval_query->add_sql_clause( 'from', self::get_db_table_name() );
+ $this->interval_query->add_sql_clause( 'group_by', 'time_interval' );
+ }
+}
diff --git a/includes/Analytics/Reports/Products/QueryFilter.php b/includes/Analytics/Reports/Products/QueryFilter.php
new file mode 100644
index 0000000000..8548f1a20a
--- /dev/null
+++ b/includes/Analytics/Reports/Products/QueryFilter.php
@@ -0,0 +1,33 @@
+clear_all_clauses();
+ $this->total_query = new WcSqlQuery( $this->context . '_total' );
+ $this->total_query->add_sql_clause( 'from', self::get_db_table_name() );
+
+ $this->interval_query = new WcSqlQuery( $this->context . '_interval' );
+ $this->interval_query->add_sql_clause( 'from', self::get_db_table_name() );
+ $this->interval_query->add_sql_clause( 'group_by', 'time_interval' );
+ }
+}
diff --git a/includes/Analytics/Reports/Stock/QueryFilter.php b/includes/Analytics/Reports/Stock/QueryFilter.php
new file mode 100644
index 0000000000..3cdbae665a
--- /dev/null
+++ b/includes/Analytics/Reports/Stock/QueryFilter.php
@@ -0,0 +1,53 @@
+get_route() === '/wc-analytics/reports/stock' ) {
+ add_filter( 'posts_clauses', array( $this, 'add_author_clause' ), 10, 2 );
+ }
+
+ return $result;
+ }
+
+ /**
+ * Apply seller ID query param to where SQL Clause.
+ *
+ * @param WP_Query $wp_query
+ * @return array
+ */
+ public function add_author_clause( $args, $wp_query ) {
+ global $wpdb;
+
+ $seller_id = $this->get_seller_id();
+
+ if ( $seller_id ) {
+ $args['where'] = $args['where'] . $wpdb->prepare(
+ " AND {$wpdb->posts}.post_author = %d ",
+ $seller_id
+ );
+ }
+
+ return $args;
+ }
+}
diff --git a/includes/Analytics/Reports/Stock/Stats/WcDataStore.php b/includes/Analytics/Reports/Stock/Stats/WcDataStore.php
new file mode 100644
index 0000000000..a9920e63f1
--- /dev/null
+++ b/includes/Analytics/Reports/Stock/Stats/WcDataStore.php
@@ -0,0 +1,166 @@
+get_seller_id();
+
+ $report_data = array();
+ $cache_expire = DAY_IN_SECONDS * 30;
+ // Set seller specific key.
+ $low_stock_transient_name = 'wc_admin_stock_count_lowstock' . $seller_id;
+ $low_stock_count = get_transient( $low_stock_transient_name );
+
+ if ( false === $low_stock_count ) {
+ $low_stock_count = $this->get_low_stock_count();
+ set_transient( $low_stock_transient_name, $low_stock_count, $cache_expire );
+ } else {
+ $low_stock_count = intval( $low_stock_count );
+ }
+
+ $report_data['lowstock'] = $low_stock_count;
+
+ $status_options = wc_get_product_stock_status_options();
+ foreach ( $status_options as $status => $label ) {
+ // Set seller specific key.
+ $transient_name = 'wc_admin_stock_count_' . $status . $seller_id;
+ $count = get_transient( $transient_name );
+ if ( false === $count ) {
+ $count = $this->get_count( $status );
+ set_transient( $transient_name, $count, $cache_expire );
+ } else {
+ $count = intval( $count );
+ }
+ $report_data[ $status ] = $count;
+ }
+
+ // Set seller specific key.
+ $product_count_transient_name = 'wc_admin_product_count' . $seller_id;
+ $product_count = get_transient( $product_count_transient_name );
+ if ( false === $product_count ) {
+ $product_count = $this->get_product_count();
+ set_transient( $product_count_transient_name, $product_count, $cache_expire );
+ } else {
+ $product_count = intval( $product_count );
+ }
+ $report_data['products'] = $product_count;
+ return $report_data;
+ }
+
+ /**
+ * Get low stock count (products with stock < low stock amount, but greater than no stock amount).
+ *
+ * @return int Low stock count.
+ */
+ protected function get_low_stock_count() {
+ global $wpdb;
+
+ $no_stock_amount = absint( max( get_option( 'woocommerce_notify_no_stock_amount' ), 0 ) );
+ $low_stock_amount = absint( max( get_option( 'woocommerce_notify_low_stock_amount' ), 1 ) );
+ $seller_where = $this->get_seller_where_query();
+
+ return (int) $wpdb->get_var(
+ $wpdb->prepare(
+ "
+ SELECT count( DISTINCT posts.ID ) FROM {$wpdb->posts} posts
+ LEFT JOIN {$wpdb->wc_product_meta_lookup} wc_product_meta_lookup ON posts.ID = wc_product_meta_lookup.product_id
+ LEFT JOIN {$wpdb->postmeta} low_stock_amount_meta ON posts.ID = low_stock_amount_meta.post_id AND low_stock_amount_meta.meta_key = '_low_stock_amount'
+ WHERE posts.post_type IN ( 'product', 'product_variation' )
+ AND wc_product_meta_lookup.stock_quantity IS NOT NULL
+ AND wc_product_meta_lookup.stock_status = 'instock'
+ AND (
+ (
+ low_stock_amount_meta.meta_value > ''
+ AND wc_product_meta_lookup.stock_quantity <= CAST(low_stock_amount_meta.meta_value AS SIGNED)
+ AND wc_product_meta_lookup.stock_quantity > %d
+ )
+ OR (
+ (
+ low_stock_amount_meta.meta_value IS NULL OR low_stock_amount_meta.meta_value <= ''
+ )
+ AND wc_product_meta_lookup.stock_quantity <= %d
+ AND wc_product_meta_lookup.stock_quantity > %d
+ )
+ )
+ {$seller_where}
+ ",
+ $no_stock_amount,
+ $low_stock_amount,
+ $no_stock_amount
+ )
+ );
+ }
+
+ /**
+ * Get count for the passed in stock status.
+ *
+ * @param string $status Status slug.
+ * @return int Count.
+ */
+ protected function get_count( $status ) {
+ global $wpdb;
+
+ $seller_where = $this->get_seller_where_query();
+
+ return (int) $wpdb->get_var(
+ $wpdb->prepare(// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
+ "
+ SELECT count( DISTINCT posts.ID ) FROM {$wpdb->posts} posts
+ LEFT JOIN {$wpdb->wc_product_meta_lookup} wc_product_meta_lookup ON posts.ID = wc_product_meta_lookup.product_id
+ WHERE posts.post_type IN ( 'product', 'product_variation' )
+ AND wc_product_meta_lookup.stock_status = %s {$seller_where}
+ ",
+ $status
+ )
+ );
+ }
+
+ /**
+ * Get product count for the store.
+ *
+ * @return int Product count.
+ */
+ protected function get_product_count() {
+ $query_args = array();
+ $query_args['post_type'] = array( 'product', 'product_variation' );
+ $seller_id = $this->get_seller_id();
+
+ if ( $seller_id ) {
+ $query_args['author'] = $seller_id;
+ }
+
+ $query = new \WP_Query();
+ $query->query( $query_args );
+
+ return intval( $query->found_posts );
+ }
+
+ protected function get_seller_id(): int {
+ return (int) dokan()->get_container()->get( \WeDevs\Dokan\Analytics\Reports\Stock\QueryFilter::class )->get_seller_id();
+ }
+
+ protected function get_seller_where_query() {
+ $seller_id = $this->get_seller_id();
+ $where = '';
+
+ if ( $seller_id ) {
+ global $wpdb;
+
+ $where = $wpdb->prepare(
+ ' AND posts.post_author = %d ',
+ $seller_id
+ );
+ }
+
+ return $where;
+ }
+}
diff --git a/includes/Analytics/Reports/Taxes/QueryFilter.php b/includes/Analytics/Reports/Taxes/QueryFilter.php
new file mode 100644
index 0000000000..8dac5502da
--- /dev/null
+++ b/includes/Analytics/Reports/Taxes/QueryFilter.php
@@ -0,0 +1,33 @@
+context ) {
+ return $column;
+ }
+
+ $table_name = $this->get_dokan_table();
+ $types = $this->get_non_refund_order_types_to_include();
+
+ $column['orders_count'] = "SUM( CASE WHEN {$table_name}.order_type IN ($types) THEN 1 ELSE 0 END ) as orders_count";
+
+ return $column;
+ }
+}
diff --git a/includes/Analytics/Reports/Taxes/Stats/WcDataStore.php b/includes/Analytics/Reports/Taxes/Stats/WcDataStore.php
new file mode 100644
index 0000000000..357efba06a
--- /dev/null
+++ b/includes/Analytics/Reports/Taxes/Stats/WcDataStore.php
@@ -0,0 +1,28 @@
+clear_all_clauses();
+ unset( $this->subquery );
+ $this->total_query = new WcSqlQuery( $this->context . '_total' );
+ $this->total_query->add_sql_clause( 'from', self::get_db_table_name() );
+
+ $this->interval_query = new WcSqlQuery( $this->context . '_interval' );
+ $this->interval_query->add_sql_clause( 'from', self::get_db_table_name() );
+ $this->interval_query->add_sql_clause( 'group_by', 'time_interval' );
+ }
+}
diff --git a/includes/Analytics/Reports/Variations/QueryFilter.php b/includes/Analytics/Reports/Variations/QueryFilter.php
new file mode 100644
index 0000000000..16615bb88d
--- /dev/null
+++ b/includes/Analytics/Reports/Variations/QueryFilter.php
@@ -0,0 +1,33 @@
+services as $class ) {
+ $implements_more = class_implements( $class );
+ if ( $implements_more ) {
+ $implements = array_merge( $implements, $implements_more );
+ }
+ }
+
+ $implements = array_unique( $implements );
+
+ return array_key_exists( $alias, $implements );
+ }
+
+ /**
+ * Register a class in the container and add tags for all the interfaces it implements.
+ *
+ * This also updates the `$this->provides` property with the interfaces provided by the class, and ensures
+ * that the property doesn't contain duplicates.
+ *
+ * @param string $id Entry ID (typically a class or interface name).
+ * @param mixed|null $concrete Concrete entity to register under that ID, null for automatic creation.
+ * @param bool|null $shared Whether to register the class as shared (`get` always returns the same instance)
+ * or not.
+ *
+ * @return DefinitionInterface
+ */
+ protected function add_with_implements_tags( string $id, $concrete = null, bool $shared = null ): DefinitionInterface {
+ $definition = $this->getContainer()->add( $id, $concrete, $shared );
+
+ foreach ( class_implements( $id ) as $interface ) {
+ $definition->addTag( $interface );
+ }
+
+ return $definition;
+ }
+
+ /**
+ * Register a shared class in the container and add tags for all the interfaces it implements.
+ *
+ * @param string $id Entry ID (typically a class or interface name).
+ * @param mixed|null $concrete Concrete entity to register under that ID, null for automatic creation.
+ *
+ * @return DefinitionInterface
+ */
+ protected function share_with_implements_tags( string $id, $concrete = null ): DefinitionInterface {
+ return $this->add_with_implements_tags( $id, $concrete, true );
+ }
+}
diff --git a/includes/DependencyManagement/BootableServiceProvider.php b/includes/DependencyManagement/BootableServiceProvider.php
new file mode 100644
index 0000000000..b07bfadd98
--- /dev/null
+++ b/includes/DependencyManagement/BootableServiceProvider.php
@@ -0,0 +1,19 @@
+invokeInit( $instance );
+ return $instance;
+ }
+
+ /**
+ * Invoke methods on resolved instance, including 'init'.
+ *
+ * @param object $instance The concrete to invoke methods on.
+ *
+ * @return object
+ */
+ protected function invokeMethods( $instance ): object {
+ $this->invokeInit( $instance );
+ parent::invokeMethods( $instance );
+ return $instance;
+ }
+
+ /**
+ * Invoke the 'init' method on a resolved object.
+ *
+ * Constructor injection causes backwards compatibility problems
+ * so we will rely on method injection via an internal method.
+ *
+ * @param object $instance The resolved object.
+ * @return void
+ */
+ private function invokeInit( $instance ) {
+ $resolved = $this->resolveArguments( $this->arguments );
+
+ if ( method_exists( $instance, static::INJECTION_METHOD ) ) {
+ call_user_func_array( array( $instance, static::INJECTION_METHOD ), $resolved );
+ }
+ }
+
+ /**
+ * Forget the cached resolved object, so the next time it's requested
+ * it will be resolved again.
+ */
+ public function forgetResolved() {
+ $this->resolved = null;
+ }
+}
diff --git a/includes/DependencyManagement/Providers/AdminServiceProvider.php b/includes/DependencyManagement/Providers/AdminServiceProvider.php
new file mode 100644
index 0000000000..34c48ab984
--- /dev/null
+++ b/includes/DependencyManagement/Providers/AdminServiceProvider.php
@@ -0,0 +1,58 @@
+services, true );
+ }
+
+ /**
+ * Register the classes.
+ */
+ public function register(): void {
+ $this->getContainer()
+ ->addShared( \WeDevs\Dokan\Admin\Hooks::class, \WeDevs\Dokan\Admin\Hooks::class )
+ ->addTag( self::TAG );
+
+ $this->getContainer()
+ ->addShared( \WeDevs\Dokan\Admin\Menu::class, \WeDevs\Dokan\Admin\Menu::class )
+ ->addTag( self::TAG );
+
+ $this->getContainer()
+ ->addShared( \WeDevs\Dokan\Admin\AdminBar::class, \WeDevs\Dokan\Admin\AdminBar::class )
+ ->addTag( self::TAG );
+
+ $this->getContainer()
+ ->addShared( \WeDevs\Dokan\Admin\Pointers::class, \WeDevs\Dokan\Admin\Pointers::class )
+ ->addTag( self::TAG );
+
+ $this->getContainer()
+ ->addShared( \WeDevs\Dokan\Admin\Settings::class, \WeDevs\Dokan\Admin\Settings::class )
+ ->addTag( self::TAG );
+
+ $this->getContainer()
+ ->addShared( \WeDevs\Dokan\Admin\UserProfile::class, \WeDevs\Dokan\Admin\UserProfile::class )
+ ->addTag( self::TAG );
+
+ $this->getContainer()
+ ->addShared( \WeDevs\Dokan\Admin\SetupWizard::class, \WeDevs\Dokan\Admin\SetupWizard::class )
+ ->addTag( self::TAG );
+ }
+}
diff --git a/includes/DependencyManagement/Providers/AjaxServiceProvider.php b/includes/DependencyManagement/Providers/AjaxServiceProvider.php
new file mode 100644
index 0000000000..e76b59337f
--- /dev/null
+++ b/includes/DependencyManagement/Providers/AjaxServiceProvider.php
@@ -0,0 +1,34 @@
+services, true );
+ }
+
+ /**
+ * Register the classes.
+ */
+ public function register(): void {
+ $this->getContainer()
+ ->addShared( \WeDevs\Dokan\Ajax::class, \WeDevs\Dokan\Ajax::class )
+ ->addTag( self::TAG );
+ }
+}
diff --git a/includes/DependencyManagement/Providers/AnalyticsServiceProvider.php b/includes/DependencyManagement/Providers/AnalyticsServiceProvider.php
new file mode 100644
index 0000000000..70d9c9ae66
--- /dev/null
+++ b/includes/DependencyManagement/Providers/AnalyticsServiceProvider.php
@@ -0,0 +1,65 @@
+services, true ) || in_array( $alias, self::TAGS, true );
+ }
+
+ /**
+ * Register the classes.
+ */
+ public function register(): void {
+ foreach ( $this->services as $service ) {
+ $definition = $this->getContainer()
+ ->addShared(
+ $service, function () use ( $service ) {
+ return new $service();
+ }
+ );
+ $this->add_tags( $definition, self::TAGS );
+ }
+ }
+
+ private function add_tags( DefinitionInterface $definition, $tags ) {
+ foreach ( $tags as $tag ) {
+ $definition = $definition->addTag( $tag );
+ }
+ }
+}
diff --git a/includes/DependencyManagement/Providers/CommonServiceProvider.php b/includes/DependencyManagement/Providers/CommonServiceProvider.php
new file mode 100644
index 0000000000..4d59f0ca24
--- /dev/null
+++ b/includes/DependencyManagement/Providers/CommonServiceProvider.php
@@ -0,0 +1,66 @@
+services, true );
+ }
+
+ /**
+ * Register the classes.
+ */
+ public function register(): void {
+ $this->getContainer()
+ ->addShared( \WeDevs\Dokan\Withdraw\Hooks::class, \WeDevs\Dokan\Withdraw\Hooks::class )
+ ->addTag( self::TAG );
+
+ $this->getContainer()
+ ->addShared( \WeDevs\Dokan\Product\Hooks::class, \WeDevs\Dokan\Product\Hooks::class )
+ ->addTag( self::TAG );
+
+ $this->getContainer()
+ ->addShared( \WeDevs\Dokan\ProductCategory\Hooks::class, \WeDevs\Dokan\ProductCategory\Hooks::class )
+ ->addTag( self::TAG );
+
+ $this->getContainer()
+ ->addShared( \WeDevs\Dokan\Upgrade\Hooks::class, \WeDevs\Dokan\Upgrade\Hooks::class )
+ ->addTag( self::TAG );
+
+ $this->getContainer()
+ ->addShared( \WeDevs\Dokan\Vendor\Hooks::class, \WeDevs\Dokan\Vendor\Hooks::class )
+ ->addTag( self::TAG );
+
+ $this->getContainer()
+ ->addShared( \WeDevs\Dokan\Vendor\UserSwitch::class, \WeDevs\Dokan\Vendor\UserSwitch::class )
+ ->addTag( self::TAG );
+
+ $this->getContainer()
+ ->addShared( \WeDevs\Dokan\CacheInvalidate::class, \WeDevs\Dokan\CacheInvalidate::class )
+ ->addTag( self::TAG );
+
+ $this->getContainer()
+ ->addShared( \WeDevs\Dokan\Shipping\Hooks::class, \WeDevs\Dokan\Shipping\Hooks::class )
+ ->addTag( self::TAG );
+
+ $this->getContainer()
+ ->addShared( \WeDevs\Dokan\Privacy::class, \WeDevs\Dokan\Privacy::class )
+ ->addTag( self::TAG );
+ }
+}
diff --git a/includes/DependencyManagement/Providers/FrontendServiceProvider.php b/includes/DependencyManagement/Providers/FrontendServiceProvider.php
new file mode 100644
index 0000000000..5eb6681f15
--- /dev/null
+++ b/includes/DependencyManagement/Providers/FrontendServiceProvider.php
@@ -0,0 +1,38 @@
+services, true );
+ }
+
+ /**
+ * Register the classes.
+ */
+ public function register(): void {
+ $this->getContainer()
+ ->addShared( \WeDevs\Dokan\Vendor\StoreListsFilter::class, \WeDevs\Dokan\Vendor\StoreListsFilter::class )
+ ->addTag( self::TAG );
+
+ $this->getContainer()
+ ->addShared( \WeDevs\Dokan\ThemeSupport\Manager::class, \WeDevs\Dokan\ThemeSupport\Manager::class )
+ ->addTag( self::TAG );
+ }
+}
diff --git a/includes/DependencyManagement/Providers/ServiceProvider.php b/includes/DependencyManagement/Providers/ServiceProvider.php
new file mode 100644
index 0000000000..1a465b5f64
--- /dev/null
+++ b/includes/DependencyManagement/Providers/ServiceProvider.php
@@ -0,0 +1,78 @@
+ \WeDevs\Dokan\Blocks\ProductBlock::class,
+ 'pageview' => \WeDevs\Dokan\PageViews::class,
+ 'seller_wizard' => \WeDevs\Dokan\Vendor\SetupWizard::class,
+ 'core' => \WeDevs\Dokan\Core::class,
+ 'scripts' => \WeDevs\Dokan\Assets::class,
+ 'email' => \WeDevs\Dokan\Emails\Manager::class,
+ 'vendor' => \WeDevs\Dokan\Vendor\Manager::class,
+ 'product' => \WeDevs\Dokan\Product\Manager::class,
+ 'shortcodes' => \WeDevs\Dokan\Shortcodes\Shortcodes::class,
+ 'registration' => \WeDevs\Dokan\Registration::class,
+ 'order' => \WeDevs\Dokan\Order\Manager::class,
+ 'order_controller' => \WeDevs\Dokan\Order\Controller::class,
+ 'api' => \WeDevs\Dokan\REST\Manager::class,
+ 'withdraw' => \WeDevs\Dokan\Withdraw\Manager::class,
+ 'dashboard' => \WeDevs\Dokan\Dashboard\Manager::class,
+ 'commission' => \WeDevs\Dokan\Commission::class,
+ 'customizer' => \WeDevs\Dokan\Customizer::class,
+ 'upgrades' => \WeDevs\Dokan\Upgrade\Manager::class,
+ 'product_sections' => \WeDevs\Dokan\ProductSections\Manager::class,
+ 'reverse_withdrawal' => \WeDevs\Dokan\ReverseWithdrawal\ReverseWithdrawal::class,
+ 'dummy_data_importer' => \WeDevs\Dokan\DummyData\Importer::class,
+ 'catalog_mode' => \WeDevs\Dokan\CatalogMode\Controller::class,
+ 'bg_process' => \WeDevs\Dokan\BackgroundProcess\Manager::class,
+ 'frontend_manager' => \WeDevs\Dokan\Frontend\Frontend::class,
+ 'rewrite' => \WeDevs\Dokan\Rewrites::class,
+ 'widgets' => \WeDevs\Dokan\Widgets\Manager::class,
+ 'admin_notices' => \WeDevs\Dokan\Admin\Notices\Manager::class,
+ 'tracker' => \WeDevs\Dokan\Tracker::class,
+ ];
+
+ /**
+ * @inheritDoc
+ *
+ * @return void
+ */
+ public function boot(): void {
+ $this->getContainer()->addServiceProvider( new AdminServiceProvider() );
+ $this->getContainer()->addServiceProvider( new CommonServiceProvider() );
+ $this->getContainer()->addServiceProvider( new FrontendServiceProvider() );
+ $this->getContainer()->addServiceProvider( new AjaxServiceProvider() );
+ $this->getContainer()->addServiceProvider( new AnalyticsServiceProvider() );
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * Check if the service provider can provide the given service alias.
+ *
+ * @param string $alias The service alias to check.
+ * @return bool True if the service provider can provide the service, false otherwise.
+ */
+ public function provides( string $alias ): bool {
+ if ( isset( $this->services[ $alias ] ) ) {
+ return true;
+ }
+
+ return parent::provides( $alias );
+ }
+
+ /**
+ * Register the classes.
+ */
+ public function register(): void {
+ foreach ( $this->services as $key => $class_name ) {
+ $this->getContainer()->addShared( $key, $class_name )->addTag( self::TAG );
+ }
+ }
+}
diff --git a/includes/Install/Installer.php b/includes/Install/Installer.php
index ee3ceeb14c..52104da3d2 100755
--- a/includes/Install/Installer.php
+++ b/includes/Install/Installer.php
@@ -327,6 +327,7 @@ public function create_tables() {
$this->create_refund_table();
$this->create_vendor_balance_table();
$this->create_reverse_withdrawal_table();
+ $this->create_dokan_order_stats_table();
}
/**
@@ -536,4 +537,31 @@ private static function parse_update_notice( $content, $new_version ) {
return wp_kses_post( $upgrade_notice );
}
+
+ public function create_dokan_order_stats_table() {
+ // Following imported here because this method could be called from the others file.
+ include_once ABSPATH . 'wp-admin/includes/upgrade.php';
+
+ global $wpdb;
+
+ $sql = "CREATE TABLE IF NOT EXISTS `{$wpdb->prefix}dokan_order_stats` (
+ `order_id` bigint UNSIGNED NOT NULL,
+ `seller_id` bigint UNSIGNED NOT NULL DEFAULT '0',
+ `order_type` tinyint(1) NOT NULL DEFAULT '0' COMMENT '0 = Dokan Parent Order, 1 = Dokan Single Vendor Order, 2 = Dokan Suborder, 3 = Refund of Dokan Parent Order, 4 = Refund of Dokan Suborder, 5 = Refund of Dokan Single Order',
+ `seller_earning` double NOT NULL DEFAULT '0',
+ `seller_gateway_fee` double NOT NULL DEFAULT '0',
+ `seller_shipping_fee` double NOT NULL DEFAULT '0',
+ `seller_discount` double NOT NULL DEFAULT '0',
+ `admin_commission` double NOT NULL DEFAULT '0',
+ `admin_gateway_fee` double NOT NULL DEFAULT '0',
+ `admin_shipping_fee` double NOT NULL DEFAULT '0',
+ `admin_discount` double NOT NULL DEFAULT '0',
+ `admin_subsidy` double NOT NULL DEFAULT '0',
+ PRIMARY KEY (order_id),
+ KEY seller_id (seller_id),
+ KEY order_type (order_type)
+ ) ENGINE=InnoDB {$wpdb->get_charset_collate()};";
+
+ dbDelta( $sql );
+ }
}
diff --git a/includes/Order/MiscHooks.php b/includes/Order/MiscHooks.php
index a4b897530b..60cd36abc0 100644
--- a/includes/Order/MiscHooks.php
+++ b/includes/Order/MiscHooks.php
@@ -22,13 +22,6 @@ class MiscHooks {
* @since 3.8.0
*/
public function __construct() {
- //Wc remove child order from wc_order_product_lookup & trim child order from posts for analytics
- add_action( 'wc-admin_import_orders', [ $this, 'delete_child_order_from_wc_order_product' ] );
-
- // Exclude suborders in woocommerce analytics.
- add_filter( 'woocommerce_analytics_orders_select_query', [ $this, 'trim_child_order_for_analytics_order' ] );
- add_filter( 'woocommerce_analytics_update_order_stats_data', [ $this, 'trim_child_order_for_analytics_order_stats' ], 10, 2 );
-
// remove customer info from order export based on setting
add_filter( 'dokan_csv_export_headers', [ $this, 'hide_customer_info_from_vendor_order_export' ], 20, 1 );
@@ -36,46 +29,6 @@ public function __construct() {
add_filter( 'wp_count_posts', [ $this, 'modify_vendor_order_counts' ], 10, 1 ); // no need to add hpos support for this filter
}
- /**
- * Delete_child_order_from_wc_order_product
- *
- * @since 3.8.0 Moved this method from Order/Hooks.php file
- *
- * @param \ActionScheduler_Action $args
- *
- * @return void
- */
- public function delete_child_order_from_wc_order_product( $args ) {
- $order = wc_get_order( $args );
-
- if ( $order->get_parent_id() ) {
- global $wpdb;
- // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
- $wpdb->delete( $wpdb->prefix . 'wc_order_product_lookup', [ 'order_id' => $order->get_id() ] );
- // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
- $wpdb->delete( $wpdb->prefix . 'wc_order_stats', [ 'order_id' => $order->get_id() ] );
- }
- }
-
- /**
- * Trim child order if parent exist from wc_order_product_lookup for analytics order
- *
- * @since 3.8.0 Moved this method from Order/Hooks.php file
- *
- * @param WC_Order $orders
- *
- * @return WC_Order
- */
- public function trim_child_order_for_analytics_order( $orders ) {
- foreach ( $orders->data as $key => $order ) {
- if ( $order['parent_id'] ) {
- unset( $orders->data[ $key ] );
- }
- }
-
- return $orders;
- }
-
/**
* Remove customer sensitive information while exporting order
*
@@ -195,28 +148,4 @@ public function modify_vendor_order_counts( $counts ) {
return $counts;
}
-
- /**
- * Exclude suborders and include dokan subscription product orders when generate woocommerce analytics data.
- *
- * @see https://github.com/getdokan/dokan-pro/issues/2735
- *
- * @param array $data
- * @param \WC_Order $order
- *
- * @return array
- */
- public function trim_child_order_for_analytics_order_stats( $data, $order ) {
- if ( ! $order->get_parent_id() ||
- (
- dokan()->is_pro_exists()
- && dokan_pro()->module->is_active( 'product_subscription' )
- && \DokanPro\Modules\Subscription\Helper::is_vendor_subscription_order( $order )
- )
- ) {
- return $data;
- }
-
- return [];
- }
}
diff --git a/includes/Upgrade/Manager.php b/includes/Upgrade/Manager.php
index cb622d8efa..eae16fd326 100644
--- a/includes/Upgrade/Manager.php
+++ b/includes/Upgrade/Manager.php
@@ -32,7 +32,7 @@ public function is_upgrade_required() {
* @return bool
*/
public function has_ongoing_process() {
- return ! ! get_option( $this->is_upgrading_db_key, false );
+ return (bool) get_option( $this->is_upgrading_db_key, false );
}
/**
@@ -112,4 +112,3 @@ public function do_upgrade() {
do_action( 'dokan_upgrade_finished' );
}
}
-
diff --git a/includes/Upgrade/Upgrades.php b/includes/Upgrade/Upgrades.php
index f12d269472..a81110997f 100644
--- a/includes/Upgrade/Upgrades.php
+++ b/includes/Upgrade/Upgrades.php
@@ -43,6 +43,7 @@ class Upgrades {
'3.6.5' => Upgrades\V_3_6_5::class,
'3.7.10' => Upgrades\V_3_7_10::class,
'3.7.19' => Upgrades\V_3_7_19::class,
+ '3.13.0' => Upgrades\V_3_13_0::class,
];
/**
diff --git a/includes/Upgrade/Upgrades/V_3_13_0.php b/includes/Upgrade/Upgrades/V_3_13_0.php
new file mode 100644
index 0000000000..3620a0c75a
--- /dev/null
+++ b/includes/Upgrade/Upgrades/V_3_13_0.php
@@ -0,0 +1,17 @@
+create_dokan_order_stats_table();
+
+ // Sync the WC order stats.
+ $import = ReportsSync::regenerate_report_data( null, false );
+ }
+}
diff --git a/lib/packages/League/Container/Argument/ArgumentInterface.php b/lib/packages/League/Container/Argument/ArgumentInterface.php
new file mode 100644
index 0000000000..bc3c864e35
--- /dev/null
+++ b/lib/packages/League/Container/Argument/ArgumentInterface.php
@@ -0,0 +1,13 @@
+getContainer();
+ } catch (ContainerException $e) {
+ $container = ($this instanceof ReflectionContainer) ? $this : null;
+ }
+
+ foreach ($arguments as &$arg) {
+ // if we have a literal, we don't want to do anything more with it
+ if ($arg instanceof LiteralArgumentInterface) {
+ $arg = $arg->getValue();
+ continue;
+ }
+
+ if ($arg instanceof ArgumentInterface) {
+ $argValue = $arg->getValue();
+ } else {
+ $argValue = $arg;
+ }
+
+ if (!is_string($argValue)) {
+ continue;
+ }
+
+ // resolve the argument from the container, if it happens to be another
+ // argument wrapper, use that value
+ if ($container instanceof ContainerInterface && $container->has($argValue)) {
+ try {
+ $arg = $container->get($argValue);
+
+ if ($arg instanceof ArgumentInterface) {
+ $arg = $arg->getValue();
+ }
+
+ continue;
+ } catch (NotFoundException $e) {
+ }
+ }
+
+ // if we have a default value, we use that, no more resolution as
+ // we expect a default/optional argument value to be literal
+ if ($arg instanceof DefaultValueInterface) {
+ $arg = $arg->getDefaultValue();
+ }
+ }
+
+ return $arguments;
+ }
+
+ public function reflectArguments(ReflectionFunctionAbstract $method, array $args = []): array
+ {
+ $params = $method->getParameters();
+ $arguments = [];
+
+ foreach ($params as $param) {
+ $name = $param->getName();
+
+ // if we've been given a value for the argument, treat as literal
+ if (array_key_exists($name, $args)) {
+ $arguments[] = new LiteralArgument($args[$name]);
+ continue;
+ }
+
+ $type = $param->getType();
+
+ if ($type instanceof ReflectionNamedType) {
+ // in PHP 8, nullable arguments have "?" prefix
+ $typeHint = ltrim($type->getName(), '?');
+
+ if ($param->isDefaultValueAvailable()) {
+ $arguments[] = new DefaultValueArgument($typeHint, $param->getDefaultValue());
+ continue;
+ }
+
+ $arguments[] = new ResolvableArgument($typeHint);
+ continue;
+ }
+
+ if ($param->isDefaultValueAvailable()) {
+ $arguments[] = new LiteralArgument($param->getDefaultValue());
+ continue;
+ }
+
+ throw new NotFoundException(sprintf(
+ 'Unable to resolve a value for parameter (%s) in the function/method (%s)',
+ $name,
+ $method->getName()
+ ));
+ }
+
+ return $this->resolveArguments($arguments);
+ }
+
+ abstract public function getContainer(): DefinitionContainerInterface;
+}
diff --git a/lib/packages/League/Container/Argument/DefaultValueArgument.php b/lib/packages/League/Container/Argument/DefaultValueArgument.php
new file mode 100644
index 0000000000..cf3c436e43
--- /dev/null
+++ b/lib/packages/League/Container/Argument/DefaultValueArgument.php
@@ -0,0 +1,24 @@
+defaultValue = $defaultValue;
+ parent::__construct($value);
+ }
+
+ /**
+ * @return mixed|null
+ */
+ public function getDefaultValue()
+ {
+ return $this->defaultValue;
+ }
+}
diff --git a/lib/packages/League/Container/Argument/DefaultValueInterface.php b/lib/packages/League/Container/Argument/DefaultValueInterface.php
new file mode 100644
index 0000000000..879fe903a4
--- /dev/null
+++ b/lib/packages/League/Container/Argument/DefaultValueInterface.php
@@ -0,0 +1,13 @@
+value = $value;
+ } else {
+ throw new InvalidArgumentException('Incorrect type for value.');
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getValue()
+ {
+ return $this->value;
+ }
+}
diff --git a/lib/packages/League/Container/Argument/LiteralArgumentInterface.php b/lib/packages/League/Container/Argument/LiteralArgumentInterface.php
new file mode 100644
index 0000000000..4cb06ff980
--- /dev/null
+++ b/lib/packages/League/Container/Argument/LiteralArgumentInterface.php
@@ -0,0 +1,9 @@
+value = $value;
+ }
+
+ public function getValue(): string
+ {
+ return $this->value;
+ }
+}
diff --git a/lib/packages/League/Container/Argument/ResolvableArgumentInterface.php b/lib/packages/League/Container/Argument/ResolvableArgumentInterface.php
new file mode 100644
index 0000000000..e7136e6c4b
--- /dev/null
+++ b/lib/packages/League/Container/Argument/ResolvableArgumentInterface.php
@@ -0,0 +1,10 @@
+definitions = $definitions ?? new DefinitionAggregate();
+ $this->providers = $providers ?? new ServiceProviderAggregate();
+ $this->inflectors = $inflectors ?? new InflectorAggregate();
+
+ if ($this->definitions instanceof ContainerAwareInterface) {
+ $this->definitions->setContainer($this);
+ }
+
+ if ($this->providers instanceof ContainerAwareInterface) {
+ $this->providers->setContainer($this);
+ }
+
+ if ($this->inflectors instanceof ContainerAwareInterface) {
+ $this->inflectors->setContainer($this);
+ }
+ }
+
+ public function add(string $id, $concrete = null): DefinitionInterface
+ {
+ $concrete = $concrete ?? $id;
+
+ if (true === $this->defaultToShared) {
+ return $this->addShared($id, $concrete);
+ }
+
+ return $this->definitions->add($id, $concrete);
+ }
+
+ public function addShared(string $id, $concrete = null): DefinitionInterface
+ {
+ $concrete = $concrete ?? $id;
+ return $this->definitions->addShared($id, $concrete);
+ }
+
+ public function defaultToShared(bool $shared = true): ContainerInterface
+ {
+ $this->defaultToShared = $shared;
+ return $this;
+ }
+
+ public function extend(string $id): DefinitionInterface
+ {
+ if ($this->providers->provides($id)) {
+ $this->providers->register($id);
+ }
+
+ if ($this->definitions->has($id)) {
+ return $this->definitions->getDefinition($id);
+ }
+
+ throw new NotFoundException(sprintf(
+ 'Unable to extend alias (%s) as it is not being managed as a definition',
+ $id
+ ));
+ }
+
+ public function addServiceProvider(ServiceProviderInterface $provider): DefinitionContainerInterface
+ {
+ $this->providers->add($provider);
+ return $this;
+ }
+
+ /**
+ * @template RequestedType
+ *
+ * @param class-string|string $id
+ *
+ * @return RequestedType|mixed
+ */
+ public function get($id)
+ {
+ return $this->resolve($id);
+ }
+
+ /**
+ * @template RequestedType
+ *
+ * @param class-string|string $id
+ *
+ * @return RequestedType|mixed
+ */
+ public function getNew($id)
+ {
+ return $this->resolve($id, true);
+ }
+
+ public function has($id): bool
+ {
+ if ($this->definitions->has($id)) {
+ return true;
+ }
+
+ if ($this->definitions->hasTag($id)) {
+ return true;
+ }
+
+ if ($this->providers->provides($id)) {
+ return true;
+ }
+
+ foreach ($this->delegates as $delegate) {
+ if ($delegate->has($id)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ public function inflector(string $type, callable $callback = null): InflectorInterface
+ {
+ return $this->inflectors->add($type, $callback);
+ }
+
+ public function delegate(ContainerInterface $container): self
+ {
+ $this->delegates[] = $container;
+
+ if ($container instanceof ContainerAwareInterface) {
+ $container->setContainer($this);
+ }
+
+ return $this;
+ }
+
+ protected function resolve($id, bool $new = false)
+ {
+ if ($this->definitions->has($id)) {
+ $resolved = (true === $new) ? $this->definitions->resolveNew($id) : $this->definitions->resolve($id);
+ return $this->inflectors->inflect($resolved);
+ }
+
+ if ($this->definitions->hasTag($id)) {
+ $arrayOf = (true === $new)
+ ? $this->definitions->resolveTaggedNew($id)
+ : $this->definitions->resolveTagged($id);
+
+ array_walk($arrayOf, function (&$resolved) {
+ $resolved = $this->inflectors->inflect($resolved);
+ });
+
+ return $arrayOf;
+ }
+
+ if ($this->providers->provides($id)) {
+ $this->providers->register($id);
+
+ if (!$this->definitions->has($id) && !$this->definitions->hasTag($id)) {
+ throw new ContainerException(sprintf('Service provider lied about providing (%s) service', $id));
+ }
+
+ return $this->resolve($id, $new);
+ }
+
+ foreach ($this->delegates as $delegate) {
+ if ($delegate->has($id)) {
+ $resolved = $delegate->get($id);
+ return $this->inflectors->inflect($resolved);
+ }
+ }
+
+ throw new NotFoundException(sprintf('Alias (%s) is not being managed by the container or delegates', $id));
+ }
+}
diff --git a/lib/packages/League/Container/ContainerAwareInterface.php b/lib/packages/League/Container/ContainerAwareInterface.php
new file mode 100644
index 0000000000..494ff2b244
--- /dev/null
+++ b/lib/packages/League/Container/ContainerAwareInterface.php
@@ -0,0 +1,11 @@
+container = $container;
+
+ if ($this instanceof ContainerAwareInterface) {
+ return $this;
+ }
+
+ throw new BadMethodCallException(sprintf(
+ 'Attempt to use (%s) while not implementing (%s)',
+ ContainerAwareTrait::class,
+ ContainerAwareInterface::class
+ ));
+ }
+
+ public function getContainer(): DefinitionContainerInterface
+ {
+ if ($this->container instanceof DefinitionContainerInterface) {
+ return $this->container;
+ }
+
+ throw new ContainerException('No container implementation has been set.');
+ }
+}
diff --git a/lib/packages/League/Container/Definition/Definition.php b/lib/packages/League/Container/Definition/Definition.php
new file mode 100644
index 0000000000..df0fb488e6
--- /dev/null
+++ b/lib/packages/League/Container/Definition/Definition.php
@@ -0,0 +1,238 @@
+alias = $id;
+ $this->concrete = $concrete;
+ }
+
+ public function addTag(string $tag): DefinitionInterface
+ {
+ $this->tags[$tag] = true;
+ return $this;
+ }
+
+ public function hasTag(string $tag): bool
+ {
+ return isset($this->tags[$tag]);
+ }
+
+ public function setAlias(string $id): DefinitionInterface
+ {
+ $this->alias = $id;
+ return $this;
+ }
+
+ public function getAlias(): string
+ {
+ return $this->alias;
+ }
+
+ public function setShared(bool $shared = true): DefinitionInterface
+ {
+ $this->shared = $shared;
+ return $this;
+ }
+
+ public function isShared(): bool
+ {
+ return $this->shared;
+ }
+
+ public function getConcrete()
+ {
+ return $this->concrete;
+ }
+
+ public function setConcrete($concrete): DefinitionInterface
+ {
+ $this->concrete = $concrete;
+ $this->resolved = null;
+ return $this;
+ }
+
+ public function addArgument($arg): DefinitionInterface
+ {
+ $this->arguments[] = $arg;
+ return $this;
+ }
+
+ public function addArguments(array $args): DefinitionInterface
+ {
+ foreach ($args as $arg) {
+ $this->addArgument($arg);
+ }
+
+ return $this;
+ }
+
+ public function addMethodCall(string $method, array $args = []): DefinitionInterface
+ {
+ $this->methods[] = [
+ 'method' => $method,
+ 'arguments' => $args
+ ];
+
+ return $this;
+ }
+
+ public function addMethodCalls(array $methods = []): DefinitionInterface
+ {
+ foreach ($methods as $method => $args) {
+ $this->addMethodCall($method, $args);
+ }
+
+ return $this;
+ }
+
+ public function resolve()
+ {
+ if (null !== $this->resolved && $this->isShared()) {
+ return $this->resolved;
+ }
+
+ return $this->resolveNew();
+ }
+
+ public function resolveNew()
+ {
+ $concrete = $this->concrete;
+
+ if (is_callable($concrete)) {
+ $concrete = $this->resolveCallable($concrete);
+ }
+
+ if ($concrete instanceof LiteralArgumentInterface) {
+ $this->resolved = $concrete->getValue();
+ return $concrete->getValue();
+ }
+
+ if ($concrete instanceof ArgumentInterface) {
+ $concrete = $concrete->getValue();
+ }
+
+ if (is_string($concrete) && class_exists($concrete)) {
+ $concrete = $this->resolveClass($concrete);
+ }
+
+ if (is_object($concrete)) {
+ $concrete = $this->invokeMethods($concrete);
+ }
+
+ try {
+ $container = $this->getContainer();
+ } catch (ContainerException $e) {
+ $container = null;
+ }
+
+ // stop recursive resolving
+ if (is_string($concrete) && in_array($concrete, $this->recursiveCheck)) {
+ $this->resolved = $concrete;
+ return $concrete;
+ }
+
+ // if we still have a string, try to pull it from the container
+ // this allows for `alias -> alias -> ... -> concrete
+ if (is_string($concrete) && $container instanceof ContainerInterface && $container->has($concrete)) {
+ $this->recursiveCheck[] = $concrete;
+ $concrete = $container->get($concrete);
+ }
+
+ $this->resolved = $concrete;
+ return $concrete;
+ }
+
+ /**
+ * @param callable $concrete
+ * @return mixed
+ */
+ protected function resolveCallable(callable $concrete)
+ {
+ $resolved = $this->resolveArguments($this->arguments);
+ return call_user_func_array($concrete, $resolved);
+ }
+
+ protected function resolveClass(string $concrete): object
+ {
+ $resolved = $this->resolveArguments($this->arguments);
+ $reflection = new ReflectionClass($concrete);
+ return $reflection->newInstanceArgs($resolved);
+ }
+
+ protected function invokeMethods(object $instance): object
+ {
+ foreach ($this->methods as $method) {
+ $args = $this->resolveArguments($method['arguments']);
+ $callable = [$instance, $method['method']];
+ call_user_func_array($callable, $args);
+ }
+
+ return $instance;
+ }
+}
diff --git a/lib/packages/League/Container/Definition/DefinitionAggregate.php b/lib/packages/League/Container/Definition/DefinitionAggregate.php
new file mode 100644
index 0000000000..96976f6990
--- /dev/null
+++ b/lib/packages/League/Container/Definition/DefinitionAggregate.php
@@ -0,0 +1,117 @@
+definitions = array_filter($definitions, static function ($definition) {
+ return ($definition instanceof DefinitionInterface);
+ });
+ }
+
+ public function add(string $id, $definition): DefinitionInterface
+ {
+ if (false === ($definition instanceof DefinitionInterface)) {
+ $definition = new Definition($id, $definition);
+ }
+
+ $this->definitions[] = $definition->setAlias($id);
+
+ return $definition;
+ }
+
+ public function addShared(string $id, $definition): DefinitionInterface
+ {
+ $definition = $this->add($id, $definition);
+ return $definition->setShared(true);
+ }
+
+ public function has(string $id): bool
+ {
+ foreach ($this->getIterator() as $definition) {
+ if ($id === $definition->getAlias()) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ public function hasTag(string $tag): bool
+ {
+ foreach ($this->getIterator() as $definition) {
+ if ($definition->hasTag($tag)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ public function getDefinition(string $id): DefinitionInterface
+ {
+ foreach ($this->getIterator() as $definition) {
+ if ($id === $definition->getAlias()) {
+ return $definition->setContainer($this->getContainer());
+ }
+ }
+
+ throw new NotFoundException(sprintf('Alias (%s) is not being handled as a definition.', $id));
+ }
+
+ public function resolve(string $id)
+ {
+ return $this->getDefinition($id)->resolve();
+ }
+
+ public function resolveNew(string $id)
+ {
+ return $this->getDefinition($id)->resolveNew();
+ }
+
+ public function resolveTagged(string $tag): array
+ {
+ $arrayOf = [];
+
+ foreach ($this->getIterator() as $definition) {
+ if ($definition->hasTag($tag)) {
+ $arrayOf[] = $definition->setContainer($this->getContainer())->resolve();
+ }
+ }
+
+ return $arrayOf;
+ }
+
+ public function resolveTaggedNew(string $tag): array
+ {
+ $arrayOf = [];
+
+ foreach ($this->getIterator() as $definition) {
+ if ($definition->hasTag($tag)) {
+ $arrayOf[] = $definition->setContainer($this->getContainer())->resolveNew();
+ }
+ }
+
+ return $arrayOf;
+ }
+
+ public function getIterator(): Generator
+ {
+ yield from $this->definitions;
+ }
+}
diff --git a/lib/packages/League/Container/Definition/DefinitionAggregateInterface.php b/lib/packages/League/Container/Definition/DefinitionAggregateInterface.php
new file mode 100644
index 0000000000..13eca48995
--- /dev/null
+++ b/lib/packages/League/Container/Definition/DefinitionAggregateInterface.php
@@ -0,0 +1,21 @@
+type = $type;
+ $this->callback = $callback;
+ }
+
+ public function getType(): string
+ {
+ return $this->type;
+ }
+
+ public function invokeMethod(string $name, array $args): InflectorInterface
+ {
+ $this->methods[$name] = $args;
+ return $this;
+ }
+
+ public function invokeMethods(array $methods): InflectorInterface
+ {
+ foreach ($methods as $name => $args) {
+ $this->invokeMethod($name, $args);
+ }
+
+ return $this;
+ }
+
+ public function setProperty(string $property, $value): InflectorInterface
+ {
+ $this->properties[$property] = $this->resolveArguments([$value])[0];
+ return $this;
+ }
+
+ public function setProperties(array $properties): InflectorInterface
+ {
+ foreach ($properties as $property => $value) {
+ $this->setProperty($property, $value);
+ }
+
+ return $this;
+ }
+
+ public function inflect(object $object): void
+ {
+ $properties = $this->resolveArguments(array_values($this->properties));
+ $properties = array_combine(array_keys($this->properties), $properties);
+
+ // array_combine() can technically return false
+ foreach ($properties ?: [] as $property => $value) {
+ $object->{$property} = $value;
+ }
+
+ foreach ($this->methods as $method => $args) {
+ $args = $this->resolveArguments($args);
+ $callable = [$object, $method];
+ call_user_func_array($callable, $args);
+ }
+
+ if ($this->callback !== null) {
+ call_user_func($this->callback, $object);
+ }
+ }
+}
diff --git a/lib/packages/League/Container/Inflector/InflectorAggregate.php b/lib/packages/League/Container/Inflector/InflectorAggregate.php
new file mode 100644
index 0000000000..2a20fe5707
--- /dev/null
+++ b/lib/packages/League/Container/Inflector/InflectorAggregate.php
@@ -0,0 +1,44 @@
+inflectors[] = $inflector;
+ return $inflector;
+ }
+
+ public function inflect($object)
+ {
+ foreach ($this->getIterator() as $inflector) {
+ $type = $inflector->getType();
+
+ if ($object instanceof $type) {
+ $inflector->setContainer($this->getContainer());
+ $inflector->inflect($object);
+ }
+ }
+
+ return $object;
+ }
+
+ public function getIterator(): Generator
+ {
+ yield from $this->inflectors;
+ }
+}
diff --git a/lib/packages/League/Container/Inflector/InflectorAggregateInterface.php b/lib/packages/League/Container/Inflector/InflectorAggregateInterface.php
new file mode 100644
index 0000000000..9395850bf0
--- /dev/null
+++ b/lib/packages/League/Container/Inflector/InflectorAggregateInterface.php
@@ -0,0 +1,14 @@
+cacheResolutions = $cacheResolutions;
+ }
+
+ public function get($id, array $args = [])
+ {
+ if ($this->cacheResolutions === true && array_key_exists($id, $this->cache)) {
+ return $this->cache[$id];
+ }
+
+ if (!$this->has($id)) {
+ throw new NotFoundException(
+ sprintf('Alias (%s) is not an existing class and therefore cannot be resolved', $id)
+ );
+ }
+
+ $reflector = new ReflectionClass($id);
+ $construct = $reflector->getConstructor();
+
+ if ($construct && !$construct->isPublic()) {
+ throw new NotFoundException(
+ sprintf('Alias (%s) has a non-public constructor and therefore cannot be instantiated', $id)
+ );
+ }
+
+ $resolution = $construct === null
+ ? new $id()
+ : $reflector->newInstanceArgs($this->reflectArguments($construct, $args))
+ ;
+
+ if ($this->cacheResolutions === true) {
+ $this->cache[$id] = $resolution;
+ }
+
+ return $resolution;
+ }
+
+ public function has($id): bool
+ {
+ return class_exists($id);
+ }
+
+ public function call(callable $callable, array $args = [])
+ {
+ if (is_string($callable) && strpos($callable, '::') !== false) {
+ $callable = explode('::', $callable);
+ }
+
+ if (is_array($callable)) {
+ if (is_string($callable[0])) {
+ // if we have a definition container, try that first, otherwise, reflect
+ try {
+ $callable[0] = $this->getContainer()->get($callable[0]);
+ } catch (ContainerException $e) {
+ $callable[0] = $this->get($callable[0]);
+ }
+ }
+
+ $reflection = new ReflectionMethod($callable[0], $callable[1]);
+
+ if ($reflection->isStatic()) {
+ $callable[0] = null;
+ }
+
+ return $reflection->invokeArgs($callable[0], $this->reflectArguments($reflection, $args));
+ }
+
+ if (is_object($callable)) {
+ $reflection = new ReflectionMethod($callable, '__invoke');
+ return $reflection->invokeArgs($callable, $this->reflectArguments($reflection, $args));
+ }
+
+ $reflection = new ReflectionFunction(\Closure::fromCallable($callable));
+
+ return $reflection->invokeArgs($this->reflectArguments($reflection, $args));
+ }
+}
diff --git a/lib/packages/League/Container/ServiceProvider/AbstractServiceProvider.php b/lib/packages/League/Container/ServiceProvider/AbstractServiceProvider.php
new file mode 100644
index 0000000000..6b69913147
--- /dev/null
+++ b/lib/packages/League/Container/ServiceProvider/AbstractServiceProvider.php
@@ -0,0 +1,28 @@
+identifier ?? get_class($this);
+ }
+
+ public function setIdentifier(string $id): ServiceProviderInterface
+ {
+ $this->identifier = $id;
+ return $this;
+ }
+}
diff --git a/lib/packages/League/Container/ServiceProvider/BootableServiceProviderInterface.php b/lib/packages/League/Container/ServiceProvider/BootableServiceProviderInterface.php
new file mode 100644
index 0000000000..986091e2c0
--- /dev/null
+++ b/lib/packages/League/Container/ServiceProvider/BootableServiceProviderInterface.php
@@ -0,0 +1,16 @@
+providers, true)) {
+ return $this;
+ }
+
+ $provider->setContainer($this->getContainer());
+
+ if ($provider instanceof BootableServiceProviderInterface) {
+ $provider->boot();
+ }
+
+ $this->providers[] = $provider;
+ return $this;
+ }
+
+ public function provides(string $service): bool
+ {
+ foreach ($this->getIterator() as $provider) {
+ if ($provider->provides($service)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ public function getIterator(): Generator
+ {
+ yield from $this->providers;
+ }
+
+ public function register(string $service): void
+ {
+ if (false === $this->provides($service)) {
+ throw new ContainerException(
+ sprintf('(%s) is not provided by a service provider', $service)
+ );
+ }
+
+ foreach ($this->getIterator() as $provider) {
+ if (in_array($provider->getIdentifier(), $this->registered, true)) {
+ continue;
+ }
+
+ if ($provider->provides($service)) {
+ $provider->register();
+ $this->registered[] = $provider->getIdentifier();
+ }
+ }
+ }
+}
diff --git a/lib/packages/League/Container/ServiceProvider/ServiceProviderAggregateInterface.php b/lib/packages/League/Container/ServiceProvider/ServiceProviderAggregateInterface.php
new file mode 100644
index 0000000000..c66a3b8362
--- /dev/null
+++ b/lib/packages/League/Container/ServiceProvider/ServiceProviderAggregateInterface.php
@@ -0,0 +1,15 @@
+sut = new OrderType();
+ }
+
+ public function test_wc_order_is_dokan_suborder_related_method() {
+ $parent_id = $this->create_multi_vendor_order();
+ $parent_order = wc_get_order( $parent_id );
+
+ $this->assertFalse( $this->sut->is_dokan_suborder_related( $parent_order ) );
+
+ $sub_order_ids = dokan_get_suborder_ids_by( $parent_id );
+
+ foreach ( $sub_order_ids as $sub_id ) {
+ $sub_order = wc_get_order( $sub_id );
+
+ $this->assertTrue( $this->sut->is_dokan_suborder_related( $sub_order ) );
+ }
+ }
+
+ public function test_order_type_method_for_multi_vendor() {
+ $parent_id = $this->create_multi_vendor_order();
+ $parent_order = wc_get_order( $parent_id );
+
+ $this->assertEquals( $this->sut::DOKAN_PARENT_ORDER, $this->sut->get_type( $parent_order ) );
+
+ $sub_order_ids = dokan_get_suborder_ids_by( $parent_id );
+
+ foreach ( $sub_order_ids as $sub_id ) {
+ $sub_order = wc_get_order( $sub_id );
+
+ $this->assertEquals( $this->sut::DOKAN_SUBORDER, $this->sut->get_type( $sub_order ) );
+ }
+ }
+
+ public function test_order_type_method_for_single_vendor() {
+ $this->seller_id2 = $this->seller_id1;
+
+ $parent_id = $this->create_multi_vendor_order();
+ $parent_order = wc_get_order( $parent_id );
+
+ $this->assertEquals( $this->sut::DOKAN_SINGLE_ORDER, $this->sut->get_type( $parent_order ) );
+
+ $sub_order_ids = dokan_get_suborder_ids_by( $parent_id );
+
+ $this->assertNull( $sub_order_ids );
+ }
+
+ public function test_order_type_for_dokan_sub_refund() {
+ $parent_id = $this->create_multi_vendor_order();
+
+ $sub_order_ids = dokan_get_suborder_ids_by( $parent_id );
+ $sub_id = $sub_order_ids[0];
+
+ $refund = $this->create_refund( $sub_id );
+
+ $this->assertEquals( $this->sut::DOKAN_SUBORDER_REFUND, $this->sut->get_type( $refund ) );
+ }
+
+ public function test_order_type_for_wc_refund() {
+ $parent_id = $this->create_multi_vendor_order();
+
+ $refund = $this->create_refund( $parent_id );
+
+ $this->assertEquals( $this->sut::DOKAN_PARENT_ORDER_REFUND, $this->sut->get_type( $refund ) );
+ }
+
+ public function test_order_type_for_dokan_single_order_refund() {
+ $this->seller_id2 = $this->seller_id1;
+
+ $parent_id = $this->create_multi_vendor_order(); // Create single vendor order.
+
+ $refund = $this->create_refund( $parent_id );
+
+ $this->assertEquals( $this->sut::DOKAN_SINGLE_ORDER_REFUND, $this->sut->get_type( $refund ) );
+ }
+}
diff --git a/tests/php/src/Analytics/Reports/Orders/QueryFilterTest.php b/tests/php/src/Analytics/Reports/Orders/QueryFilterTest.php
new file mode 100644
index 0000000000..77754d3fea
--- /dev/null
+++ b/tests/php/src/Analytics/Reports/Orders/QueryFilterTest.php
@@ -0,0 +1,310 @@
+sut = dokan_get_container()->get( QueryFilter::class );
+ }
+
+ protected function tearDown(): void {
+ parent::tearDown();
+
+ dokan_get_container()->extend( QueryFilter::class )->setConcrete( $this->sut );
+ }
+ /**
+ * Test that the order statistics hooks are registered correctly.
+ *
+ * @see https://giuseppe-mazzapica.gitbook.io/brain-monkey/wordpress-specific-tools/wordpress-hooks-added
+ *
+ * @return void
+ */
+ public function test_orders_hook_registered() {
+ $order_stats_query_filter = dokan_get_container()->get( QueryFilter::class );
+ // Assert the Join Clause filters are registered
+ self::assertNotFalse( has_filter( 'woocommerce_analytics_clauses_join_orders_subquery', [ $order_stats_query_filter, 'add_join_subquery' ] ) );
+ // Assert the Where Clause filters are registered
+ self::assertNotFalse( has_filter( 'woocommerce_analytics_clauses_where_orders_subquery', [ $order_stats_query_filter, 'add_where_subquery' ] ) );
+ }
+
+ /**
+ * Test dokan_order_stats JOIN and WHERE clauses are applied on order_stats select Query.
+ *
+ * Method(partial) mocking @see http://docs.mockery.io/en/latest/reference/partial_mocks.html
+ *
+ * @param int $order_id The order ID.
+ * @param int $seller_id1 The first seller ID.
+ * @param int $seller_id2 The second seller ID.
+ * @return void
+ */
+ public function test_filter_hooks_are_applied_for_orders_query() {
+ $order_id = $this->create_multi_vendor_order();
+
+ $this->run_all_pending();
+
+ $mocking_methods = [
+ 'add_join_subquery',
+ 'add_where_subquery',
+ 'add_select_subquery',
+ ];
+
+ $service = Mockery::mock( QueryFilter::class . '[' . implode( ',', $mocking_methods ) . ']' );
+ dokan_get_container()->extend( QueryFilter::class )->setConcrete( $service );
+
+ foreach ( $mocking_methods as $method ) {
+ $service->shouldReceive( $method )
+ ->atLeast()
+ ->once()
+ ->andReturnUsing(
+ function ( $clauses ) {
+ return $clauses;
+ }
+ );
+ }
+
+ $wc_stats_query = new \Automattic\WooCommerce\Admin\API\Reports\Orders\Query();
+
+ $wc_stats_query->get_data();
+ }
+
+ /**
+ * @dataProvider get_dokan_stats_data
+ *
+ * @return void
+ */
+ public function test_dokan_order_stats_fields_are_selected_for_seller( $expected_data ) {
+ $order_id = $this->create_multi_vendor_order();
+
+ $this->set_order_meta_for_dokan( $order_id, $expected_data );
+
+ $this->run_all_pending();
+
+ $mocking_methods = [
+ 'should_filter_by_seller_id',
+ ];
+
+ $service = Mockery::mock( QueryFilter::class . '[' . implode( ',', $mocking_methods ) . ']' );
+
+ dokan_get_container()->extend( QueryFilter::class )->setConcrete( $service );
+
+ remove_filter( 'woocommerce_analytics_clauses_where_orders_subquery', [ $this->sut, 'add_where_subquery' ], 30 );
+
+ $service->shouldReceive( 'should_filter_by_seller_id' )
+ ->andReturnTrue();
+
+ $wc_stats_query = new \Automattic\WooCommerce\Admin\API\Reports\Orders\Query();
+
+ $data = $wc_stats_query->get_data();
+
+ $sub_ids = dokan_get_suborder_ids_by( $order_id );
+
+ $report_data = $data->data;
+
+ $this->assertEquals( count( $sub_ids ), count( $report_data ) );
+
+ foreach ( $sub_ids as $index => $s_id ) {
+ $sub_order = wc_get_order( $s_id );
+ $order_data = $report_data[ $index ];
+
+ $this->assertEquals( $s_id, $order_data['order_id'] );
+ $this->assertEquals( floatval( $sub_order->get_total() ), $order_data['total_sales'] );
+
+ foreach ( $expected_data as $key => $val ) {
+ $this->assertEquals( $val, $order_data[ $key ] );
+ }
+ }
+ }
+
+ /**
+ * @dataProvider get_dokan_stats_data
+ *
+ * @return void
+ */
+ public function test_dokan_order_stats_fields_are_selected_for_admin( $expected_data ) {
+ $order_id = $this->create_multi_vendor_order();
+
+ $this->set_order_meta_for_dokan( $order_id, $expected_data );
+
+ $this->run_all_pending();
+
+ remove_filter( 'woocommerce_analytics_clauses_where_orders_subquery', [ $this->sut, 'add_where_subquery' ], 30 );
+
+ $service = Mockery::mock( QueryFilter::class . '[should_filter_by_seller_id]' );
+ dokan_get_container()->extend( QueryFilter::class )->setConcrete( $service );
+
+ $service->shouldReceive( 'should_filter_by_seller_id' )
+ ->andReturnUsing(
+ function () {
+ return false;
+ }
+ );
+
+ $wc_stats_query = new \Automattic\WooCommerce\Admin\API\Reports\Orders\Query();
+
+ $data = $wc_stats_query->get_data();
+
+ $sub_ids = dokan_get_suborder_ids_by( $order_id );
+
+ $report_data = $data->data;
+
+ $this->assertEquals( 1, count( $report_data ) );
+
+ $report_data = $report_data[0];
+
+ foreach ( $expected_data as $key => $val ) {
+ $this->assertArrayHasKey( $key, $report_data );
+ }
+ }
+
+ public function test_orders_for_dokan_suborder_refund() {
+ $order_id = $this->create_multi_vendor_order();
+ $sub_ids = dokan_get_suborder_ids_by( $order_id );
+
+ $refund = $this->create_refund( $sub_ids[0] );
+
+ $this->run_all_pending();
+
+ remove_filter( 'woocommerce_analytics_clauses_where_orders_subquery', [ $this->sut, 'add_where_subquery' ], 30 );
+
+ $service = Mockery::mock( QueryFilter::class . '[should_filter_by_seller_id]' );
+ dokan_get_container()->extend( QueryFilter::class )->setConcrete( $service );
+
+ $service->shouldReceive( 'should_filter_by_seller_id' )
+ ->andReturnTrue();
+
+ $_GET['refunds'] = 'all';
+
+ $wc_stats_query = new \Automattic\WooCommerce\Admin\API\Reports\Orders\Query( [ 'refunds' => 'all' ] );
+ $data = $wc_stats_query->get_data();
+
+ $report_data = $data->data;
+
+ $this->assertEquals( 1, count( $report_data ) );
+
+ $report_data = $report_data[0];
+
+ $this->assertEquals( $refund->get_id(), $report_data['order_id'] );
+ }
+
+ public function test_orders_for_dokan_parent_order_refund() {
+ $order_id = $this->create_multi_vendor_order();
+ $sub_ids = dokan_get_suborder_ids_by( $order_id );
+
+ $parent_refund = $this->create_refund( $sub_ids[0], true, true );
+
+ $this->run_all_pending();
+
+ remove_filter( 'woocommerce_analytics_clauses_where_orders_subquery', [ $this->sut, 'add_where_subquery' ], 30 );
+
+ $service = Mockery::mock( QueryFilter::class . '[should_filter_by_seller_id]' );
+ dokan_get_container()->extend( QueryFilter::class )->setConcrete( $service );
+
+ $service->shouldReceive( 'should_filter_by_seller_id' )
+ ->andReturnFalse();
+
+ $_GET['refunds'] = 'all';
+
+ $wc_stats_query = new \Automattic\WooCommerce\Admin\API\Reports\Orders\Query( [ 'refunds' => 'all' ] );
+ $data = $wc_stats_query->get_data();
+
+ $report_data = $data->data;
+
+ $this->assertEquals( 1, count( $report_data ) );
+
+ $report_data = $report_data[0];
+
+ $this->assertEquals( $parent_refund->get_id(), $report_data['order_id'] );
+ }
+
+ public function test_orders_for_dokan_single_order_refund() {
+ $this->seller_id2 = $this->seller_id1;
+
+ $order_id = $this->create_multi_vendor_order();
+
+ $refund = $this->create_refund( $order_id );
+
+ $this->run_all_pending();
+
+ remove_filter( 'woocommerce_analytics_clauses_where_orders_subquery', [ $this->sut, 'add_where_subquery' ], 30 );
+
+ $service = Mockery::mock( QueryFilter::class . '[should_filter_by_seller_id]' );
+ dokan_get_container()->extend( QueryFilter::class )->setConcrete( $service );
+
+ $service->shouldReceive( 'should_filter_by_seller_id' )
+ ->andReturnFalse();
+
+ $_GET['refunds'] = 'all';
+
+ $wc_stats_query = new \Automattic\WooCommerce\Admin\API\Reports\Orders\Query( [ 'refunds' => 'all' ] );
+ $data = $wc_stats_query->get_data();
+
+ $report_data = $data->data;
+
+ $this->assertEquals( 1, count( $report_data ) );
+
+ $report_data = $report_data[0];
+
+ $this->assertEquals( $refund->get_id(), $report_data['order_id'] );
+ }
+
+ public function test_orders_analytics_for_seller_filter_as_a_admin() {
+ $order_id = $this->create_single_vendor_order( $this->seller_id1 );
+ $order_id2 = $this->create_single_vendor_order( $this->seller_id2 );
+
+ $this->run_all_pending();
+
+ $_GET['sellers'] = $this->seller_id1;
+
+ wp_set_current_user( $this->admin_id );
+
+ $wc_stats_query = new \Automattic\WooCommerce\Admin\API\Reports\Orders\Query();
+
+ $data = $wc_stats_query->get_data();
+
+ $sub_ids = dokan_get_suborder_ids_by( $order_id );
+
+ $report_data = $data->data;
+
+ $this->assertEquals( 1, count( $report_data ) );
+ $this->assertEquals( $order_id, $report_data[0]['order_id'] );
+ }
+
+ public function test_orders_analytics_for_seller_filter_as_a_vendor() {
+ $order_id = $this->create_single_vendor_order( $this->seller_id1 );
+ $order_id2 = $this->create_single_vendor_order( $this->seller_id2 );
+
+ $this->run_all_pending();
+
+ // Ignore seller filter if a seller pass another seller ID as filter .
+ $_GET['seller'] = $this->seller_id1;
+
+ wp_set_current_user( $this->seller_id2 );
+
+ $wc_stats_query = new \Automattic\WooCommerce\Admin\API\Reports\Orders\Query();
+
+ $data = $wc_stats_query->get_data();
+
+ $sub_ids = dokan_get_suborder_ids_by( $order_id );
+
+ $report_data = $data->data;
+
+ $this->assertEquals( 1, count( $report_data ) );
+ $this->assertEquals( $order_id2, $report_data[0]['order_id'] );
+ }
+}
diff --git a/tests/php/src/Analytics/Reports/Orders/Stats/QueryFilterTest.php b/tests/php/src/Analytics/Reports/Orders/Stats/QueryFilterTest.php
new file mode 100644
index 0000000000..b08052d396
--- /dev/null
+++ b/tests/php/src/Analytics/Reports/Orders/Stats/QueryFilterTest.php
@@ -0,0 +1,166 @@
+sut = dokan_get_container()->get( QueryFilter::class );
+ }
+
+ protected function tearDown(): void {
+ parent::tearDown();
+ dokan_get_container()->extend( QueryFilter::class )->setConcrete( $this->sut );
+ }
+
+ /**
+ * Test that the order statistics hooks are registered correctly.
+ *
+ * @see https://giuseppe-mazzapica.gitbook.io/brain-monkey/wordpress-specific-tools/wordpress-hooks-added
+ *
+ * @return void
+ */
+ public function test_order_stats_hook_registered() {
+ $order_stats_query_filter = dokan_get_container()->get( QueryFilter::class );
+ // Assert the Join Clause filters are registered
+ self::assertNotFalse( has_filter( 'woocommerce_analytics_clauses_join_orders_stats_total', [ $order_stats_query_filter, 'add_join_subquery' ] ) );
+ self::assertNotFalse( has_filter( 'woocommerce_analytics_clauses_join_orders_stats_interval', [ $order_stats_query_filter, 'add_join_subquery' ] ) );
+ // Assert the Where Clause filters are registered
+ self::assertNotFalse( has_filter( 'woocommerce_analytics_clauses_where_orders_stats_total', [ $order_stats_query_filter, 'add_where_subquery' ] ) );
+ self::assertNotFalse( has_filter( 'woocommerce_analytics_clauses_where_orders_stats_interval', [ $order_stats_query_filter, 'add_where_subquery' ] ) );
+ }
+
+
+ /**
+ * Test dokan_order_stats JOIN and WHERE clauses are applied on order_stats select Query.
+ *
+ * Method(partial) mocking @see http://docs.mockery.io/en/latest/reference/partial_mocks.html
+ *
+ * @param int $order_id The order ID.
+ * @param int $seller_id1 The first seller ID.
+ * @param int $seller_id2 The second seller ID.
+ * @return void
+ */
+ public function test_dokan_order_states_query_filter_hooks_are_order_stats_update() {
+ $order_id = $this->create_multi_vendor_order();
+
+ $this->run_all_pending();
+
+ $mocking_methods = [
+ 'add_join_subquery',
+ 'add_where_subquery',
+ 'add_select_subquery_for_total',
+ ];
+
+ $service = Mockery::mock( QueryFilter::class . '[' . implode( ',', $mocking_methods ) . ']' );
+ dokan_get_container()->extend( QueryFilter::class )->setConcrete( $service );
+
+ foreach ( $mocking_methods as $method ) {
+ $service->shouldReceive( $method )
+ ->atLeast()
+ ->once()
+ ->andReturnUsing(
+ function ( $clauses ) {
+ return $clauses;
+ }
+ );
+ }
+
+ $wc_stats_query = new \Automattic\WooCommerce\Admin\API\Reports\Orders\Stats\Query();
+
+ $wc_stats_query->get_data();
+ }
+
+ /**
+ * @dataProvider get_dokan_stats_data
+ *
+ * @return void
+ */
+ public function test_dokan_order_stats_added_to_wc_select_query_for_seller( array $data ) {
+ $parent_id = $this->create_multi_vendor_order();
+
+ $this->set_order_meta_for_dokan( $parent_id, $data );
+
+ $this->run_all_pending();
+
+ $filter = Mockery::mock( QueryFilter::class . '[should_filter_by_seller_id]' );
+
+ dokan_get_container()->extend( QueryFilter::class )->setConcrete( $filter );
+
+ $filter->shouldReceive( 'should_filter_by_seller_id' )
+ ->atLeast()
+ ->once()
+ ->andReturnTrue();
+
+ $orders_query = new \Automattic\WooCommerce\Admin\API\Reports\Orders\Stats\Query( [] );
+
+ $report_data = $orders_query->get_data();
+
+ $sub_ids = dokan_get_suborder_ids_by( $parent_id );
+
+ $this->assertCount( $report_data->totals->orders_count, $sub_ids );
+
+ $sub_ord_count = count( $sub_ids );
+
+ // Assert dokan order stats totals.
+ foreach ( $data as $key => $val ) {
+ $this->assertEquals( floatval( $val * $sub_ord_count ), $report_data->totals->{"total_$key"} );
+ }
+ }
+
+ /**
+ * @dataProvider get_dokan_stats_data
+ *
+ * @return void
+ */
+ public function test_dokan_order_stats_added_to_wc_select_query_for_admin( array $data ) {
+ $parent_id = $this->create_multi_vendor_order();
+ $this->set_order_meta_for_dokan( $parent_id, $data );
+
+ $this->run_all_pending();
+
+ $filter = Mockery::mock( QueryFilter::class . '[should_filter_by_seller_id]' );
+
+ remove_filter( 'woocommerce_analytics_clauses_where_orders_stats_total', [ $this->sut, 'add_where_subquery' ], 30 );
+ remove_filter( 'woocommerce_analytics_clauses_where_orders_stats_total', [ $this->sut, 'add_where_subquery' ], 30 );
+
+ dokan_get_container()->extend( QueryFilter::class )->setConcrete( $filter );
+
+ $filter->shouldReceive( 'should_filter_by_seller_id' )
+ ->atLeast()
+ ->once()
+ ->andReturnFalse();
+
+ $orders_query = new \Automattic\WooCommerce\Admin\API\Reports\Orders\Stats\Query( [] );
+
+ $report_data = $orders_query->get_data();
+
+ $sub_ids = dokan_get_suborder_ids_by( $parent_id );
+
+ $this->assertEquals( 1, $report_data->totals->orders_count );
+
+ $sub_ord_count = count( $sub_ids );
+
+ // Assert dokan order stats totals.
+ foreach ( $data as $key => $val ) {
+ $this->assertEquals( floatval( $val * $sub_ord_count ), $report_data->totals->{"total_$key"} );
+ }
+ }
+}
diff --git a/tests/php/src/Analytics/Reports/Orders/Stats/ScheduleListenerTest.php b/tests/php/src/Analytics/Reports/Orders/Stats/ScheduleListenerTest.php
new file mode 100644
index 0000000000..1f2f892b72
--- /dev/null
+++ b/tests/php/src/Analytics/Reports/Orders/Stats/ScheduleListenerTest.php
@@ -0,0 +1,83 @@
+get( ScheduleListener::class );
+ self::assertNotFalse( has_action( 'woocommerce_analytics_update_order_stats', [ $order_stats_table_listener, 'sync_dokan_order' ] ) );
+ }
+
+ /**
+ * Test the dokan_order_stats table update hooks are executed by ScheduleListener class.
+ *
+ * Method(partial) mocking @see http://docs.mockery.io/en/latest/reference/partial_mocks.html
+ *
+ * @param int $order_id The order ID.
+ * @param int $seller_id1 The first seller ID.
+ * @param int $seller_id2 The second seller ID.
+ * @return void
+ */
+ public function test_dokan_order_states_update_hook_execute_on_order_stats_update() {
+ $order_id = $this->create_multi_vendor_order();
+ $service = Mockery::mock( ScheduleListener::class . '[sync_dokan_order]' );
+ dokan_get_container()->extend( ScheduleListener::class )->setConcrete( $service );
+
+ $service->shouldReceive( 'sync_dokan_order' )
+ ->atLeast()
+ ->once()
+ ->andReturn( 1 );
+
+ $this->run_all_pending();
+ }
+
+ /**
+ * Test the mock class for multi-vendor order statistics.
+ *
+ * @param int $order_id The order ID.
+ * @param int $seller_id1 The first seller ID.
+ * @param int $seller_id2 The second seller ID.
+ * @return void
+ */
+ public function test_data_is_inserted_in_dokan_order_stats_table() {
+ $order_id = $this->create_multi_vendor_order();
+
+ $this->run_all_pending();
+
+ $wc_order_stats_table = \Automattic\WooCommerce\Admin\API\Reports\Orders\Stats\DataStore::get_db_table_name();
+
+ $dokan_order_stats_table = \WeDevs\Dokan\Analytics\Reports\Orders\Stats\DataStore::get_db_table_name();
+
+ $this->assertDatabaseCount(
+ $wc_order_stats_table, 1, [
+ 'order_id' => $order_id,
+ ]
+ );
+
+ $this->assertDatabaseCount(
+ $dokan_order_stats_table, 1, [
+ 'order_id' => $order_id,
+ 'order_type' => OrderType::DOKAN_PARENT_ORDER,
+ ]
+ );
+
+ $sub_order_ids = dokan_get_suborder_ids_by( $order_id );
+
+ foreach ( $sub_order_ids as $sub_id ) {
+ $this->assertDatabaseCount(
+ $dokan_order_stats_table, 1, [
+ 'order_id' => $sub_id,
+ 'order_type' => OrderType::DOKAN_SUBORDER,
+ ]
+ );
+ }
+ }
+}
diff --git a/tests/php/src/Analytics/Reports/Products/QueryFilterTest.php b/tests/php/src/Analytics/Reports/Products/QueryFilterTest.php
new file mode 100644
index 0000000000..0e3cb0678f
--- /dev/null
+++ b/tests/php/src/Analytics/Reports/Products/QueryFilterTest.php
@@ -0,0 +1,173 @@
+sut = dokan_get_container()->get( QueryFilter::class );
+ }
+
+ protected function tearDown(): void {
+ parent::tearDown();
+
+ dokan_get_container()->extend( QueryFilter::class )->setConcrete( $this->sut );
+ }
+ /**
+ * Test that the order statistics hooks are registered correctly.
+ *
+ * @see https://giuseppe-mazzapica.gitbook.io/brain-monkey/wordpress-specific-tools/wordpress-hooks-added
+ *
+ * @return void
+ */
+ public function test_products_hook_registered() {
+ $order_stats_query_filter = dokan_get_container()->get( QueryFilter::class );
+ // Assert the Join Clause filters are registered
+ self::assertNotFalse( has_filter( 'woocommerce_analytics_clauses_join_products_subquery', [ $order_stats_query_filter, 'add_join_subquery' ] ) );
+ // Assert the Where Clause filters are registered
+ self::assertNotFalse( has_filter( 'woocommerce_analytics_clauses_where_products_subquery', [ $order_stats_query_filter, 'add_where_subquery' ] ) );
+ }
+
+ /**
+ * Test dokan_order_stats JOIN and WHERE clauses are applied on order_stats select Query.
+ *
+ * Method(partial) mocking @see http://docs.mockery.io/en/latest/reference/partial_mocks.html
+ *
+ * @param int $order_id The order ID.
+ * @param int $seller_id1 The first seller ID.
+ * @param int $seller_id2 The second seller ID.
+ * @return void
+ */
+ public function test_filter_hooks_are_applied_for_products_query() {
+ $order_id = $this->create_multi_vendor_order();
+
+ $this->run_all_pending();
+
+ $mocking_methods = [
+ 'add_join_subquery',
+ 'add_where_subquery',
+ ];
+
+ $service = Mockery::mock( QueryFilter::class . '[' . implode( ',', $mocking_methods ) . ']' );
+ dokan_get_container()->extend( QueryFilter::class )->setConcrete( $service );
+
+ foreach ( $mocking_methods as $method ) {
+ $service->shouldReceive( $method )
+ ->atLeast()
+ ->once()
+ ->andReturnUsing(
+ function ( $clauses ) {
+ return $clauses;
+ }
+ );
+ }
+
+ $wc_stats_query = new \Automattic\WooCommerce\Admin\API\Reports\Products\Query();
+
+ $wc_stats_query->get_data();
+ }
+
+ /**
+ *
+ * @return void
+ */
+ public function test_dokan_products_fields_are_selected_for_seller() {
+ $order_id = $this->create_multi_vendor_order();
+
+ $this->run_all_pending();
+
+ $mocking_methods = [
+ 'should_filter_by_seller_id',
+ ];
+
+ $service = Mockery::mock( QueryFilter::class . '[' . implode( ',', $mocking_methods ) . ']' );
+
+ dokan_get_container()->extend( QueryFilter::class )->setConcrete( $service );
+
+ remove_filter( 'woocommerce_analytics_clauses_where_products_subquery', [ $this->sut, 'add_where_subquery' ], 30 );
+
+ $service->shouldReceive( 'should_filter_by_seller_id' )
+ ->andReturnTrue();
+
+ $wc_stats_query = new \Automattic\WooCommerce\Admin\API\Reports\Products\Query();
+
+ $data = $wc_stats_query->get_data();
+
+ $sub_ids = dokan_get_suborder_ids_by( $order_id );
+
+ $report_data = $data->data;
+
+ $this->assertCount( 2, $report_data );
+
+ // Assert that sub order items are fetched.
+ foreach ( $sub_ids as $s_id ) {
+ $s_order = wc_get_order( $s_id );
+
+ foreach ( $s_order->get_items() as $item ) {
+ $this->assertNestedContains(
+ [
+ 'product_id' => $item->get_product_id(),
+ 'net_revenue' => floatval( $item->get_total() ),
+ 'items_sold' => $item->get_quantity(),
+ 'orders_count' => 1,
+ ], $report_data
+ );
+ }
+ }
+ }
+
+ public function test_dokan_products_fields_are_selected_for_admin() {
+ $order_id = $this->create_multi_vendor_order();
+
+ $this->run_all_pending();
+
+ $mocking_methods = [
+ 'should_filter_by_seller_id',
+ ];
+
+ $service = Mockery::mock( QueryFilter::class . '[' . implode( ',', $mocking_methods ) . ']' );
+
+ dokan_get_container()->extend( QueryFilter::class )->setConcrete( $service );
+
+ remove_filter( 'woocommerce_analytics_clauses_where_products_subquery', [ $this->sut, 'add_where_subquery' ], 30 );
+
+ $service->shouldReceive( 'should_filter_by_seller_id' )
+ ->andReturnFalse();
+
+ $wc_stats_query = new \Automattic\WooCommerce\Admin\API\Reports\Products\Query();
+
+ $data = $wc_stats_query->get_data();
+
+ $report_data = $data->data;
+
+ $this->assertCount( 2, $report_data );
+
+ // Assert that parent order items are fetched.
+ $s_order = wc_get_order( $order_id );
+
+ foreach ( $s_order->get_items() as $item ) {
+ $this->assertNestedContains(
+ [
+ 'product_id' => $item->get_product_id(),
+ 'net_revenue' => floatval( $item->get_total() ),
+ 'items_sold' => $item->get_quantity(),
+ 'orders_count' => 1,
+ ], $report_data
+ );
+ }
+ }
+}
diff --git a/tests/php/src/Analytics/Reports/Products/Stats/QueryFilterTest.php b/tests/php/src/Analytics/Reports/Products/Stats/QueryFilterTest.php
new file mode 100644
index 0000000000..b7a1459cd7
--- /dev/null
+++ b/tests/php/src/Analytics/Reports/Products/Stats/QueryFilterTest.php
@@ -0,0 +1,169 @@
+sut = dokan_get_container()->get( QueryFilter::class );
+ }
+
+ protected function tearDown(): void {
+ parent::tearDown();
+ dokan_get_container()->extend( QueryFilter::class )->setConcrete( $this->sut );
+ }
+
+ /**
+ * Test that the order statistics hooks are registered correctly.
+ *
+ * @see https://giuseppe-mazzapica.gitbook.io/brain-monkey/wordpress-specific-tools/wordpress-hooks-added
+ *
+ * @return void
+ */
+ public function test_products_stats_hook_registered() {
+ $order_stats_query_filter = dokan_get_container()->get( QueryFilter::class );
+ // Assert the Join Clause filters are registered
+ self::assertNotFalse( has_filter( 'woocommerce_analytics_clauses_join_products_stats_total', [ $order_stats_query_filter, 'add_join_subquery' ] ) );
+ self::assertNotFalse( has_filter( 'woocommerce_analytics_clauses_join_products_stats_interval', [ $order_stats_query_filter, 'add_join_subquery' ] ) );
+ // Assert the Where Clause filters are registered
+ self::assertNotFalse( has_filter( 'woocommerce_analytics_clauses_where_products_stats_total', [ $order_stats_query_filter, 'add_where_subquery' ] ) );
+ self::assertNotFalse( has_filter( 'woocommerce_analytics_clauses_where_products_stats_interval', [ $order_stats_query_filter, 'add_where_subquery' ] ) );
+ }
+
+ /**
+ * Test dokan_order_stats JOIN and WHERE clauses are applied on order_stats select Query.
+ *
+ * Method(partial) mocking @see http://docs.mockery.io/en/latest/reference/partial_mocks.html
+ *
+ * @return void
+ */
+ public function test_dokan_products_states_query_filter_hooks_are_order_stats_update() {
+ $order_id = $this->create_multi_vendor_order();
+
+ $this->run_all_pending();
+
+ $mocking_methods = [
+ 'add_join_subquery',
+ 'add_where_subquery',
+ ];
+
+ $service = Mockery::mock( QueryFilter::class . '[' . implode( ',', $mocking_methods ) . ']' );
+ dokan_get_container()->extend( QueryFilter::class )->setConcrete( $service );
+
+ foreach ( $mocking_methods as $method ) {
+ $service->shouldReceive( $method )
+ ->atLeast()
+ ->once()
+ ->andReturnUsing(
+ function ( $clauses ) {
+ return $clauses;
+ }
+ );
+ }
+
+ $wc_stats_query = new \Automattic\WooCommerce\Admin\API\Reports\Products\Stats\Query();
+
+ $wc_stats_query->get_data();
+ }
+
+ /**
+ *
+ * @return void
+ */
+ public function test_dokan_products_stats_added_to_wc_select_query_for_seller() {
+ $parent_id = $this->create_multi_vendor_order();
+
+ $this->run_all_pending();
+
+ $filter = Mockery::mock( QueryFilter::class . '[should_filter_by_seller_id]' );
+
+ dokan_get_container()->extend( QueryFilter::class )->setConcrete( $filter );
+
+ $filter->shouldReceive( 'should_filter_by_seller_id' )
+ ->atLeast()
+ ->once()
+ ->andReturnTrue();
+
+ $orders_query = new \Automattic\WooCommerce\Admin\API\Reports\Products\Stats\Query( [] );
+
+ $report_data = $orders_query->get_data();
+
+ $sub_ids = dokan_get_suborder_ids_by( $parent_id );
+
+ $this->assertCount( $report_data->totals->orders_count, $sub_ids );
+
+ $order = wc_get_order( $parent_id );
+
+ // Initialize a variable to hold the total
+ $line_items_total = 0;
+
+ // Loop through each line item in the order
+ foreach ( $order->get_items() as $item_id => $item ) {
+ // Add the line item total to the cumulative total
+ $line_items_total += $item->get_total();
+ }
+
+ $this->assertCount( $report_data->totals->orders_count, $sub_ids );
+ $this->assertEquals( $order->get_item_count(), $report_data->totals->items_sold );
+ $this->assertEquals( $line_items_total, $report_data->totals->net_revenue );
+ }
+
+ /**
+ *
+ * @return void
+ */
+ public function test_dokan_products_stats_added_to_wc_select_query_for_admin() {
+ $parent_id = $this->create_multi_vendor_order();
+
+ $this->run_all_pending();
+
+ $filter = Mockery::mock( QueryFilter::class . '[should_filter_by_seller_id]' );
+
+ remove_filter( 'woocommerce_analytics_clauses_where_products_stats_total', [ $this->sut, 'add_where_subquery' ], 30 );
+ remove_filter( 'woocommerce_analytics_clauses_where_products_stats_total', [ $this->sut, 'add_where_subquery' ], 30 );
+
+ dokan_get_container()->extend( QueryFilter::class )->setConcrete( $filter );
+
+ $filter->shouldReceive( 'should_filter_by_seller_id' )
+ ->atLeast()
+ ->once()
+ ->andReturnFalse();
+
+ $orders_query = new \Automattic\WooCommerce\Admin\API\Reports\Products\Stats\Query( [] );
+
+ $report_data = $orders_query->get_data();
+
+ $sub_ids = dokan_get_suborder_ids_by( $parent_id );
+
+ $this->assertEquals( 1, $report_data->totals->orders_count );
+
+ $order = wc_get_order( $parent_id );
+
+ // Initialize a variable to hold the total
+ $line_items_total = 0;
+
+ // Loop through each line item in the order
+ foreach ( $order->get_items() as $item_id => $item ) {
+ // Add the line item total to the cumulative total
+ $line_items_total += $item->get_total();
+ }
+
+ $this->assertEquals( $order->get_item_count(), $report_data->totals->items_sold );
+ $this->assertEquals( $line_items_total, $report_data->totals->net_revenue );
+ }
+}
diff --git a/tests/php/src/Analytics/Reports/ReportTestCase.php b/tests/php/src/Analytics/Reports/ReportTestCase.php
new file mode 100644
index 0000000000..30ba44f5e2
--- /dev/null
+++ b/tests/php/src/Analytics/Reports/ReportTestCase.php
@@ -0,0 +1,113 @@
+get_items();
+ $product_id = '';
+ $line_item = [];
+ $line_item_id = '';
+
+ foreach ( $order_items as $key => $item ) {
+ $line_item_id = $item->get_id();
+
+ $qty = $item->get_quantity() - 1;
+ $amount = $item->get_total();
+ $product_id = $item->get_product_id();
+ $line_item = array(
+ 'qty' => $qty,
+ 'refund_total' => $amount,
+ 'refund_tax' => array(),
+ );
+
+ break;
+ }
+
+ // Create the refund object.
+ $refund = wc_create_refund(
+ array(
+ 'reason' => 'Testing Refund',
+ 'order_id' => $order_id,
+ 'line_items' => [ $line_item_id => $line_item ],
+ )
+ );
+
+ if ( $parent_refund && $order->get_parent_id() ) {
+ $parent_id = $order->get_parent_id();
+ $parent_order = wc_get_order( $parent_id );
+ $order_items = $parent_order->get_items();
+ foreach ( $order_items as $key => $item ) {
+ if ( $product_id === $item->get_product_id() ) {
+ $line_item_id = $item->get_id();
+ break;
+ }
+ }
+
+ // Create the parent order refund object.
+ $parent_refund = wc_create_refund(
+ array(
+ 'reason' => 'Testing Parent Refund',
+ 'order_id' => $parent_id,
+ 'line_items' => [ $line_item_id => $line_item ],
+ )
+ );
+
+ if ( $return_parent_refund ) {
+ return $parent_refund;
+ }
+ }
+
+ return $refund;
+ }
+
+ protected function set_order_meta_for_dokan( $parent_id, array $data ) {
+ // Filter null when order is single vendor order.
+ $sub_order_ids = array_filter( (array) dokan_get_suborder_ids_by( $parent_id ) );
+
+ // Fill sub orders meta data.
+ foreach ( $sub_order_ids as $sub_id ) {
+ $sub_order = wc_get_order( $sub_id );
+
+ foreach ( $data as $key => $val ) {
+ $sub_order->add_meta_data( '_' . $key, $val, true );
+ }
+
+ $sub_order->save_meta_data();
+ $sub_order->save();
+ }
+
+ // Fill parent order meta data
+ $order = wc_get_order( $parent_id );
+ $count = count( $sub_order_ids ) ? count( $sub_order_ids ) : 1;
+
+ foreach ( $data as $key => $val ) {
+ $order->add_meta_data( '_' . $key, $val * $count, true );
+ }
+
+ $order->save_meta_data();
+ $order->save();
+ }
+
+ public static function get_dokan_stats_data() {
+ return [
+ [
+ [
+ 'seller_earning' => random_int( 5, 10 ),
+ 'seller_gateway_fee' => random_int( 5, 10 ),
+ 'seller_discount' => random_int( 5, 10 ),
+ 'admin_commission' => random_int( 5, 10 ),
+ 'admin_gateway_fee' => random_int( 5, 10 ),
+ 'admin_discount' => random_int( 5, 10 ),
+ 'admin_subsidy' => random_int( 5, 10 ),
+ ],
+ ],
+ ];
+ }
+}
diff --git a/tests/php/src/Analytics/Reports/Stock/ProductQueryFilterTest.php b/tests/php/src/Analytics/Reports/Stock/ProductQueryFilterTest.php
new file mode 100644
index 0000000000..d670dcbf82
--- /dev/null
+++ b/tests/php/src/Analytics/Reports/Stock/ProductQueryFilterTest.php
@@ -0,0 +1,54 @@
+factory()->product->set_seller_id( $this->seller_id1 )
+ ->create_many( 5 );
+
+ $seller2_prod_ids = $this->factory()->product->set_seller_id( $this->seller_id2 )
+ ->create_many( 5 );
+
+ wp_set_current_user( $this->admin_id );
+
+ $_GET['sellers'] = $this->seller_id1;
+
+ $response = $this->get_request( 'stock', [ 'sellers' => $this->seller_id1 ] );
+
+ $data = $response->get_data();
+
+ foreach ( $data as $item ) {
+ $prod = wc_get_product( $item['id'] );
+ }
+
+ $this->assertCount( count( $seller1_prod_ids ), $data );
+ }
+
+ public function tests_stock_reports_are_fetched_without_seller_filter() {
+ $seller1_prod_ids = $this->factory()->product->set_seller_id( $this->seller_id1 )
+ ->create_many( 5 );
+
+ $seller2_prod_ids = $this->factory()->product->set_seller_id( $this->seller_id2 )
+ ->create_many( 5 );
+
+ wp_set_current_user( $this->admin_id );
+
+ $response = $this->get_request( 'stock' );
+
+ $data = $response->get_data();
+
+ foreach ( $data as $item ) {
+ $prod = wc_get_product( $item['id'] );
+ }
+
+ $this->assertCount( count( $seller1_prod_ids ) + count( $seller2_prod_ids ), $data );
+ }
+}
diff --git a/tests/php/src/Analytics/Reports/Stock/Stats/QueryFilterTest.php b/tests/php/src/Analytics/Reports/Stock/Stats/QueryFilterTest.php
new file mode 100644
index 0000000000..c6f7df4d5b
--- /dev/null
+++ b/tests/php/src/Analytics/Reports/Stock/Stats/QueryFilterTest.php
@@ -0,0 +1,53 @@
+factory()->product
+ ->set_seller_id( $this->seller_id1 )
+ ->create_many( 5 );
+
+ $seller2_prod_ids = $this->factory()->product
+ ->set_seller_id( $this->seller_id2 )
+ ->create_many( 5 );
+
+ wp_set_current_user( $this->admin_id );
+
+ $query = new \Automattic\WooCommerce\Admin\API\Reports\Stock\Stats\Query();
+
+ $data = $query->get_data();
+ $total = count( $seller1_prod_ids ) + count( $seller2_prod_ids );
+
+ $this->assertEquals( $total, $data['instock'] );
+ $this->assertEquals( $total, $data['products'] );
+ }
+
+ public function tests_stock_stats_report_by_seller_filter() {
+ $seller1_prod_ids = $this->factory()->product
+ ->set_seller_id( $this->seller_id1 )
+ ->create_many( 5 );
+
+ $seller2_prod_ids = $this->factory()->product
+ ->set_seller_id( $this->seller_id2 )
+ ->create_many( 5 );
+
+ wp_set_current_user( $this->admin_id );
+
+ $_GET['sellers'] = $this->seller_id1;
+
+ $query = new \Automattic\WooCommerce\Admin\API\Reports\Stock\Stats\Query();
+
+ $data = $query->get_data();
+ $total = count( $seller1_prod_ids );
+
+ $this->assertEquals( $total, $data['instock'] );
+ $this->assertEquals( $total, $data['products'] );
+ }
+}
diff --git a/tests/php/src/DBAssertionTrait.php b/tests/php/src/DBAssertionTrait.php
new file mode 100644
index 0000000000..34416b8a7d
--- /dev/null
+++ b/tests/php/src/DBAssertionTrait.php
@@ -0,0 +1,77 @@
+ 'val1', 'field1' => 'val1'];
+ // $placeholders = "field1='%s' AND field2='%s' ";
+ $placeholders = implode(
+ ' AND ', array_map(
+ function ( $key ) {
+ return "{$key} = %s";
+ }, array_keys( $data )
+ )
+ );
+ } else {
+ $data = [ 1 ];
+ }
+
+ if ( ! str_starts_with( $table, $wpdb->prefix ) ) {
+ $table = $wpdb->prefix . $table;
+ }
+
+ $sql = $wpdb->prepare(
+ "SELECT COUNT(*) FROM $table WHERE $placeholders ",
+ array_values( $data )
+ );
+
+ $rows_count = $wpdb->get_var( $sql );
+
+ return $rows_count;
+ }
+
+ /**
+ * Assert that a table contains at least one row matching the specified criteria.
+ *
+ * @param string $table The name of the table (without the prefix).
+ * @param array $data An associative array of field-value pairs to match.
+ * @return void
+ */
+ public function assertDatabaseHas( string $table, array $data = [] ): void {
+ $rows_count = $this->getDatabaseCount( $table, $data );
+
+ $this->assertGreaterThanOrEqual( 1, $rows_count, "No rows found in `$table` for given data " . json_encode( $data ) );
+ }
+
+ /**
+ * Assert that a table contains the specified number of rows matching the criteria.
+ *
+ * @param string $table The name of the table (without the prefix).
+ * @param int $count The expected number of matching rows.
+ * @param array $data An associative array of field-value pairs to match.
+ * @return void
+ */
+ public function assertDatabaseCount( string $table, int $count, array $data = [] ): void {
+ $rows_count = $this->getDatabaseCount( $table, $data );
+
+ $this->assertEquals( $count, $rows_count, "No rows found in `$table` for given data " . json_encode( $data ) );
+ }
+}