diff --git a/includes/API/Product_Variations_Controller.php b/includes/API/Product_Variations_Controller.php index f1cfe96..aae77fb 100644 --- a/includes/API/Product_Variations_Controller.php +++ b/includes/API/Product_Variations_Controller.php @@ -18,6 +18,7 @@ use WP_REST_Server; use WC_Product_Variation; use WP_Error; +use WCPOS\WooCommercePOS\Services\Settings; /** * Product Tgas controller class. @@ -326,15 +327,45 @@ public function wcpos_product_variation_query( array $args, WP_REST_Request $req add_filter( 'posts_groupby', array( $this, 'wcpos_posts_groupby_posts_search' ), 10, 2 ); } + // if POS only products are enabled, exclude online-only products + if ( $this->wcpos_pos_only_products_enabled() ) { + add_filter( 'posts_where', array( $this, 'wcpos_posts_where_product_variation_exclude_online_only' ), 10, 2 ); + } + // Check for wcpos_include/wcpos_exclude parameter. + // NOTE: do this after POS visibility filter so that takes precedence. if ( isset( $request['wcpos_include'] ) || isset( $request['wcpos_exclude'] ) ) { - // Add a custom WHERE clause to the query. - add_filter( 'posts_where', array( $this, 'wcpos_posts_where_product_variation_include_exclude' ), 10, 2 ); + add_filter( 'posts_where', array( $this, 'wcpos_posts_where_product_variation_include_exclude' ), 20, 2 ); } return $args; } + /** + * Filters the WHERE clause of the query. + * + * @param string $where The WHERE clause of the query. + * @param WP_Query $query The WP_Query instance (passed by reference). + * + * @return string + */ + public function wcpos_posts_where_product_variation_exclude_online_only( string $where, WP_Query $query ) { + global $wpdb; + + $settings_instance = Settings::instance(); + $online_only = $settings_instance->get_online_only_variations_visibility_settings(); + $online_only_ids = isset( $online_only['ids'] ) && is_array( $online_only['ids'] ) ? $online_only['ids'] : array(); + + // Exclude online-only product IDs if POS only products are enabled + if ( ! empty( $online_only_ids ) ) { + $online_only_ids = array_map( 'intval', (array) $online_only_ids ); + $ids_format = implode( ',', array_fill( 0, count( $online_only_ids ), '%d' ) ); + $where .= $wpdb->prepare( " AND {$wpdb->posts}.ID NOT IN ($ids_format) ", $online_only_ids ); + } + + return $where; + } + /** * Filters the WHERE clause of the query. * @@ -386,13 +417,18 @@ public function wcpos_get_all_posts( $request ) { // Initialize the SQL query. $sql = "SELECT DISTINCT {$select_fields} FROM {$wpdb->posts}"; + $sql .= " WHERE {$wpdb->posts}.post_type = 'product_variation' AND {$wpdb->posts}.post_status = 'publish'"; - // Apply '_pos_visibility' condition if necessary. + // If the '_pos_visibility' condition needs to be applied. if ( $this->wcpos_pos_only_products_enabled() ) { - $sql .= " LEFT JOIN {$wpdb->postmeta} ON ({$wpdb->posts}.ID = {$wpdb->postmeta}.post_id AND {$wpdb->postmeta}.meta_key = '_pos_visibility')"; - $sql .= " WHERE {$wpdb->posts}.post_type = 'product_variation' AND {$wpdb->posts}.post_status = 'publish' AND ({$wpdb->postmeta}.post_id IS NULL OR {$wpdb->postmeta}.meta_value != 'online_only')"; - } else { - $sql .= " WHERE {$wpdb->posts}.post_type = 'product_variation' AND {$wpdb->posts}.post_status = 'publish'"; + $settings_instance = Settings::instance(); + $online_only = $settings_instance->get_online_only_variations_visibility_settings(); + + if ( isset( $online_only['ids'] ) && is_array( $online_only['ids'] ) && ! empty( $online_only['ids'] ) ) { + $online_only_ids = array_map( 'intval', (array) $online_only['ids'] ); + $ids_format = implode( ',', array_fill( 0, count( $online_only_ids ), '%d' ) ); + $sql .= $wpdb->prepare( " AND ID NOT IN ($ids_format) ", $online_only_ids ); + } } // Add modified_after condition if provided. @@ -474,26 +510,6 @@ protected function prepare_objects_query( $request ) { } } - // Add online_only check - if ( $this->wcpos_pos_only_products_enabled() ) { - $meta_query = array( - 'relation' => 'OR', - array( - 'key' => '_pos_visibility', - 'compare' => 'NOT EXISTS', - ), - array( - 'key' => '_pos_visibility', - 'value' => 'online_only', - 'compare' => '!=', - ), - ); - - // Combine meta queries - $args['meta_query'] = $this->wcpos_combine_meta_queries( $args['meta_query'], $meta_query ); - - }; - return $args; } diff --git a/includes/API/Products_Controller.php b/includes/API/Products_Controller.php index 35a5a4b..cc8fa20 100644 --- a/includes/API/Products_Controller.php +++ b/includes/API/Products_Controller.php @@ -18,6 +18,7 @@ use WP_REST_Request; use WP_REST_Response; use WP_Error; +use WCPOS\WooCommercePOS\Services\Settings; /** * Products controller class. @@ -352,10 +353,14 @@ public function wcpos_product_query( array $args, WP_REST_Request $request ) { add_filter( 'posts_groupby', array( $this, 'wcpos_posts_groupby_product_search' ), 10, 2 ); } + // if POS only products are enabled, exclude online-only products + if ( $this->wcpos_pos_only_products_enabled() ) { + add_filter( 'posts_where', array( $this, 'wcpos_posts_where_product_exclude_online_only' ), 10, 2 ); + } + // Check for wcpos_include/wcpos_exclude parameter. if ( isset( $request['wcpos_include'] ) || isset( $request['wcpos_exclude'] ) ) { - // Add a custom WHERE clause to the query. - add_filter( 'posts_where', array( $this, 'wcpos_posts_where_product_include_exclude' ), 10, 2 ); + add_filter( 'posts_where', array( $this, 'wcpos_posts_where_product_include_exclude' ), 20, 2 ); } return $args; @@ -397,6 +402,31 @@ public function wcpos_posts_groupby_product_search( string $groupby, WP_Query $q return $groupby; } + /** + * Filters the WHERE clause of the query. + * + * @param string $where The WHERE clause of the query. + * @param WP_Query $query The WP_Query instance (passed by reference). + * + * @return string + */ + public function wcpos_posts_where_product_exclude_online_only( string $where, WP_Query $query ) { + global $wpdb; + + $settings_instance = Settings::instance(); + $online_only = $settings_instance->get_online_only_product_visibility_settings(); + $online_only_ids = isset( $online_only['ids'] ) && is_array( $online_only['ids'] ) ? $online_only['ids'] : array(); + + // Exclude online-only product IDs if POS only products are enabled + if ( ! empty( $online_only_ids ) ) { + $online_only_ids = array_map( 'intval', (array) $online_only_ids ); + $ids_format = implode( ',', array_fill( 0, count( $online_only_ids ), '%d' ) ); + $where .= $wpdb->prepare( " AND {$wpdb->posts}.ID NOT IN ($ids_format) ", $online_only_ids ); + } + + return $where; + } + /** * Filters the WHERE clause of the query. * @@ -447,13 +477,17 @@ public function wcpos_get_all_posts( $request ) { // Use SELECT DISTINCT in the initial SQL statement for both cases. $sql = "SELECT DISTINCT {$select_fields} FROM {$wpdb->posts}"; + $sql .= " WHERE post_type = 'product' AND post_status = 'publish'"; // If the '_pos_visibility' condition needs to be applied. if ( $this->wcpos_pos_only_products_enabled() ) { - $sql .= " LEFT JOIN {$wpdb->postmeta} ON ({$wpdb->posts}.ID = {$wpdb->postmeta}.post_id AND {$wpdb->postmeta}.meta_key = '_pos_visibility')"; - $sql .= " WHERE post_type = 'product' AND post_status = 'publish' AND ({$wpdb->postmeta}.post_id IS NULL OR {$wpdb->postmeta}.meta_value != 'online_only')"; - } else { - $sql .= " WHERE post_type = 'product' AND post_status = 'publish'"; + $settings_instance = Settings::instance(); + $online_only = $settings_instance->get_online_only_product_visibility_settings(); + if ( isset( $online_only['ids'] ) && is_array( $online_only['ids'] ) && ! empty( $online_only['ids'] ) ) { + $online_only_ids = array_map( 'intval', (array) $online_only['ids'] ); + $ids_format = implode( ',', array_fill( 0, count( $online_only_ids ), '%d' ) ); + $sql .= $wpdb->prepare( " AND ID NOT IN ($ids_format) ", $online_only_ids ); + } } // Add modified_after condition if provided. @@ -530,24 +564,6 @@ protected function prepare_objects_query( $request ) { } } - // Add online_only check - if ( $this->wcpos_pos_only_products_enabled() ) { - $meta_query = array( - 'relation' => 'OR', - array( - 'key' => '_pos_visibility', - 'compare' => 'NOT EXISTS', - ), - array( - 'key' => '_pos_visibility', - 'value' => 'online_only', - 'compare' => '!=', - ), - ); - - $args['meta_query'] = $this->wcpos_combine_meta_queries( $meta_query, $args['meta_query'] ); - }; - return $args; } } diff --git a/includes/Activator.php b/includes/Activator.php index fb142dc..40d1f34 100644 --- a/includes/Activator.php +++ b/includes/Activator.php @@ -252,6 +252,7 @@ private function db_upgrade( $old, $current ): void { '0.4' => 'updates/update-0.4.php', '0.4.6' => 'updates/update-0.4.6.php', '1.0.0-beta.1' => 'updates/update-1.0.0-beta.1.php', + '1.6.1' => 'updates/update-1.6.1.php', ); foreach ( $db_updates as $version => $updater ) { if ( version_compare( $version, $old, '>' ) && diff --git a/includes/Admin/Products/List_Products.php b/includes/Admin/Products/List_Products.php index 1e4a15a..7335e45 100644 --- a/includes/Admin/Products/List_Products.php +++ b/includes/Admin/Products/List_Products.php @@ -1,5 +1,4 @@ get_pos_only_product_visibility_settings(); + $pos_only_ids = isset( $pos_only['ids'] ) && is_array( $pos_only['ids'] ) ? array_map( 'intval', (array) $pos_only['ids'] ) : array(); + $online_only = $settings_instance->get_online_only_product_visibility_settings(); + $online_only_ids = isset( $online_only['ids'] ) && is_array( $online_only['ids'] ) ? array_map( 'intval', (array) $online_only['ids'] ) : array(); + $new_views = array(); foreach ( $visibility_filters as $key => $label ) { - $sql = $wpdb->prepare( - "SELECT count(DISTINCT pm.post_id) - FROM {$wpdb->postmeta} pm - JOIN {$wpdb->posts} p ON (p.ID = pm.post_id) - WHERE pm.meta_key = '_pos_visibility' - AND pm.meta_value = %s - AND p.post_type = 'product' - AND p.post_status = 'publish' - ", - $key - ); - $count = $wpdb->get_var( $sql ); + $count = 0; + $ids = array(); + $format = ''; + + if ( 'pos_only' === $key ) { + $ids = $pos_only_ids; + $format = implode( ',', array_fill( 0, count( $pos_only_ids ), '%d' ) ); + } elseif ( 'online_only' === $key ) { + $ids = $online_only_ids; + $format = implode( ',', array_fill( 0, count( $online_only_ids ), '%d' ) ); + } + + if ( ! empty( $ids ) ) { + $sql = "SELECT count(DISTINCT ID) FROM {$wpdb->posts} WHERE post_type = 'product'"; + $sql .= $wpdb->prepare( " AND ID IN ($format) ", $ids ); + $count = $wpdb->get_var( $sql ); + } $class = ( isset( $_GET['pos_visibility'] ) && $_GET['pos_visibility'] == $key ) ? 'current' : ''; $query_string = remove_query_arg( array( 'pos_visibility', 'post_status' ) ); @@ -160,27 +176,51 @@ public function pos_visibility_filters( array $views ): array { /** - * Show/hide POS products. + * Modify the SQL clauses for the query to filter by POS visibility. * + * @param array $clauses * @param WP_Query $query + * + * @return array */ - public function pre_get_posts( WP_Query $query ): void { - // Ensure we're in the admin and it's the main query - if ( ! is_admin() && ! $query->is_main_query() ) { - return; + public function posts_clauses( array $clauses, WP_Query $query ): array { + // Ensure we're in the admin and it's the main query. + if ( ! is_admin() || ! $query->is_main_query() ) { + return $clauses; } - // If 'pos_visibility' filter is set - if ( ! empty( $_GET['pos_visibility'] ) ) { - $meta_query = array( - array( - 'key' => '_pos_visibility', - 'value' => sanitize_text_field( wp_unslash( $_GET['pos_visibility'] ) ), - ), - ); + // If 'pos_visibility' filter is set. + if ( empty( $_GET['pos_visibility'] ) ) { + return $clauses; + } - $query->set( 'meta_query', $meta_query ); + global $wpdb; + $visibility = sanitize_text_field( wp_unslash( $_GET['pos_visibility'] ) ); + $settings_instance = Settings::instance(); + + if ( 'pos_only' === $visibility ) { + $pos_only = $settings_instance->get_pos_only_product_visibility_settings(); + $pos_only_ids = isset( $pos_only['ids'] ) && is_array( $pos_only['ids'] ) ? array_map( 'intval', (array) $pos_only['ids'] ) : array(); + $format = implode( ',', array_fill( 0, count( $pos_only_ids ), '%d' ) ); + if ( empty( $pos_only_ids ) ) { + // No IDs, show no records. + $clauses['where'] .= ' AND 1=0 '; + } else { + $clauses['where'] .= $wpdb->prepare( " AND ID IN ($format) ", $pos_only_ids ); + } + } elseif ( 'online_only' === $visibility ) { + $online_only = $settings_instance->get_online_only_product_visibility_settings(); + $online_only_ids = isset( $online_only['ids'] ) && is_array( $online_only['ids'] ) ? array_map( 'intval', (array) $online_only['ids'] ) : array(); + $format = implode( ',', array_fill( 0, count( $online_only_ids ), '%d' ) ); + if ( empty( $online_only_ids ) ) { + // No IDs, show no records. + $clauses['where'] .= ' AND 1=0 '; + } else { + $clauses['where'] .= $wpdb->prepare( " AND ID IN ($format) ", $online_only_ids ); + } } + + return $clauses; } /** @@ -219,8 +259,13 @@ public function quick_edit( $column_name, $post_type ): void { */ public static function quick_edit_save( WC_Product $product ): void { if ( ! empty( $_POST['_pos_visibility'] ) ) { - $product->update_meta_data( '_pos_visibility', sanitize_text_field( $_POST['_pos_visibility'] ) ); - $product->save(); + $settings_instance = Settings::instance(); + $args = array( + 'post_type' => 'products', + 'visibility' => sanitize_text_field( $_POST['_pos_visibility'] ), + 'ids' => array( $product->get_id() ), + ); + $settings_instance->update_visibility_settings( $args ); } } @@ -231,8 +276,13 @@ public static function quick_edit_save( WC_Product $product ): void { */ public function bulk_edit_save( WC_Product $product ): void { if ( ! empty( $_GET['_pos_visibility'] ) ) { - $product->update_meta_data( '_pos_visibility', sanitize_text_field( $_GET['_pos_visibility'] ) ); - $product->save(); + $settings_instance = Settings::instance(); + $args = array( + 'post_type' => 'products', + 'visibility' => sanitize_text_field( $_GET['_pos_visibility'] ), + 'ids' => array( $product->get_id() ), + ); + $settings_instance->update_visibility_settings( $args ); } } @@ -242,7 +292,18 @@ public function bulk_edit_save( WC_Product $product ): void { */ public function custom_product_column( $column, $post_id ): void { if ( 'name' == $column ) { - $selected = get_post_meta( $post_id, '_pos_visibility', true ); + $selected = ''; + $settings_instance = Settings::instance(); + $pos_only = $settings_instance->is_product_pos_only( $post_id ); + $online_only = $settings_instance->is_product_online_only( $post_id ); + + // Set $selected based on the visibility status. + if ( $pos_only ) { + $selected = 'pos_only'; + } elseif ( $online_only ) { + $selected = 'online_only'; + } + echo '
'; } } diff --git a/includes/Admin/Products/Single_Product.php b/includes/Admin/Products/Single_Product.php index 36e80ef..b4781b4 100644 --- a/includes/Admin/Products/Single_Product.php +++ b/includes/Admin/Products/Single_Product.php @@ -13,6 +13,8 @@ namespace WCPOS\WooCommercePOS\Admin\Products; use WCPOS\WooCommercePOS\Registry; +use WCPOS\WooCommercePOS\Services\Settings; + use const DOING_AUTOSAVE; class Single_Product { @@ -185,19 +187,25 @@ public function save_post( $post_id, $post ): void { return; } - // Don't save revisions and autosaves + // Don't save revisions and autosaves. if ( wp_is_post_revision( $post_id ) || wp_is_post_autosave( $post_id ) ) { return; } - // Make sure the current user has permission to edit the post + // Make sure the current user has permission to edit the post. if ( ! current_user_can( 'edit_post', $post_id ) ) { return; } - // Get the product and save + // Get the product and save. if ( isset( $_POST['_pos_visibility'] ) ) { - update_post_meta( $post_id, '_pos_visibility', $_POST['_pos_visibility'] ); + $settings_instance = Settings::instance(); + $args = array( + 'post_type' => 'products', + 'visibility' => $_POST['_pos_visibility'], + 'ids' => array( $post_id ), + ); + $settings_instance->update_visibility_settings( $args ); } } @@ -211,7 +219,18 @@ public function post_submitbox_misc_actions(): void { return; } - $selected = get_post_meta( $post->ID, '_pos_visibility', true ); + $selected = ''; + $settings_instance = Settings::instance(); + $pos_only = $settings_instance->is_product_pos_only( $post->ID ); + $online_only = $settings_instance->is_product_online_only( $post->ID ); + + // Set $selected based on the visibility status. + if ( $pos_only ) { + $selected = 'pos_only'; + } elseif ( $online_only ) { + $selected = 'online_only'; + } + if ( ! $selected ) { $selected = ''; if ( 'add' == get_current_screen()->action ) { @@ -222,17 +241,25 @@ public function post_submitbox_misc_actions(): void { include 'templates/post-metabox-visibility-select.php'; } - /**s + /** * * @param $loop * @param $variation_data * @param $variation */ public function after_variable_attributes_pos_only_products( $loop, $variation_data, $variation ): void { - $selected = get_post_meta( $variation->ID, '_pos_visibility', true ); - if ( ! $selected ) { - $selected = ''; + $selected = ''; + $settings_instance = Settings::instance(); + $pos_only = $settings_instance->is_product_pos_only( $variation->ID ); + $online_only = $settings_instance->is_product_online_only( $variation->ID ); + + // Set $selected based on the visibility status. + if ( $pos_only ) { + $selected = 'pos_only'; + } elseif ( $online_only ) { + $selected = 'online_only'; } + include 'templates/variation-metabox-visibility-select.php'; } @@ -241,7 +268,13 @@ public function after_variable_attributes_pos_only_products( $loop, $variation_d */ public function save_product_variation_pos_only_products( $variation_id ): void { if ( isset( $_POST['variable_pos_visibility'][ $variation_id ] ) ) { - update_post_meta( $variation_id, '_pos_visibility', $_POST['variable_pos_visibility'][ $variation_id ] ); + $settings_instance = Settings::instance(); + $args = array( + 'post_type' => 'variations', + 'visibility' => $_POST['variable_pos_visibility'][ $variation_id ], + 'ids' => array( $variation_id ), + ); + $settings_instance->update_visibility_settings( $args ); } } } diff --git a/includes/Products.php b/includes/Products.php index 74ad0bb..adf2116 100644 --- a/includes/Products.php +++ b/includes/Products.php @@ -11,6 +11,7 @@ use WC_Product; use Automattic\WooCommerce\StoreApi\Exceptions\NotPurchasableException; +use WCPOS\WooCommercePOS\Services\Settings; /** * @@ -26,7 +27,7 @@ public function __construct() { $pos_only_products = woocommerce_pos_get_settings( 'general', 'pos_only_products' ); if ( $pos_only_products ) { - add_action( 'woocommerce_product_query', array( $this, 'hide_pos_only_products' ) ); + add_filter( 'posts_where', array( $this, 'hide_pos_only_products' ), 10, 2 ); add_filter( 'woocommerce_variation_is_visible', array( $this, 'hide_pos_only_variations' ), 10, 4 ); add_action( 'woocommerce_store_api_validate_add_to_cart', array( $this, 'store_api_prevent_pos_only_add_to_cart' ) ); @@ -68,43 +69,29 @@ public function product_set_stock( WC_Product $product ): void { } /** - * Hide POS Only products from the shop and category pages. + * Filters the WHERE clause of the query. * - * @TODO - this should be improved so that admin users can see the product, but get a message + * @param string $where The WHERE clause of the query. + * @param WP_Query $query The WP_Query instance (passed by reference). * - * @param WP_Query $query Query instance. - * - * @return void + * @return string */ - public function hide_pos_only_products( $query ) { - $meta_query = $query->get( 'meta_query' ); - - // Define your default meta query. - $default_meta_query = array( - 'relation' => 'OR', - array( - 'key' => '_pos_visibility', - 'value' => 'pos_only', - 'compare' => '!=', - ), - array( - 'key' => '_pos_visibility', - 'compare' => 'NOT EXISTS', - ), - ); - - // Check if an existing meta query exists. - if ( is_array( $meta_query ) ) { - if ( ! isset( $meta_query ['relation'] ) ) { - $meta_query['relation'] = 'AND'; + public function hide_pos_only_products( $where, $query ) { + // Ensure this only runs for the main WooCommerce shop queries + if ( ! is_admin() && $query->is_main_query() && ( is_shop() || is_product_category() || is_product_tag() ) ) { + global $wpdb; + + $settings_instance = Settings::instance(); + $settings = $settings_instance->get_pos_only_product_visibility_settings(); + + if ( isset( $settings['ids'] ) && ! empty( $settings['ids'] ) ) { + $exclude_ids = array_map( 'intval', (array) $settings['ids'] ); + $ids_format = implode( ',', array_fill( 0, count( $exclude_ids ), '%d' ) ); + $where .= $wpdb->prepare( " AND {$wpdb->posts}.ID NOT IN ($ids_format)", $exclude_ids ); } - $meta_query[] = $default_meta_query; - } else { - $meta_query = $default_meta_query; } - // Set the updated meta query back to the query. - $query->set( 'meta_query', $meta_query ); + return $where; } /** @@ -117,11 +104,10 @@ public function hide_pos_only_products( $query ) { */ public function hide_pos_only_variations( $visible, $variation_id, $product_id, $variation ) { if ( \is_shop() || \is_product_category() || \is_product() ) { - // Get the _pos_visibility meta value for the variation. - $pos_visibility = get_post_meta( $variation_id, '_pos_visibility', true ); + $settings_instance = Settings::instance(); + $pos_only = $settings_instance->is_variation_pos_only( $variation_id ); - // Check if _pos_visibility is 'pos_only' for this variation. - if ( $pos_visibility === 'pos_only' ) { + if ( $pos_only ) { return false; } } @@ -137,26 +123,7 @@ public function hide_pos_only_variations( $visible, $variation_id, $product_id, * @return array The modified arguments. */ public function filter_category_count_exclude_pos_only( $args ) { - if ( ! is_admin() && \function_exists( 'woocommerce_pos_get_settings' ) ) { - $pos_only_products = woocommerce_pos_get_settings( 'general', 'pos_only_products' ); - - if ( $pos_only_products ) { - $meta_query = $args['meta_query'] ?? array(); - - $meta_query['relation'] = 'OR'; - $meta_query[] = array( - 'key' => '_pos_visibility', - 'value' => 'pos_only', - 'compare' => '!=', - ); - $meta_query[] = array( - 'key' => '_pos_visibility', - 'compare' => 'NOT EXISTS', - ); - - $args['meta_query'] = $meta_query; - } - } + // @TODO: Do we need this? return $args; } @@ -172,9 +139,11 @@ public function filter_category_count_exclude_pos_only( $args ) { * @return bool */ public function prevent_pos_only_add_to_cart( $passed, $product_id ) { - $pos_visibility = get_post_meta( $product_id, '_pos_visibility', true ); + $settings_instance = Settings::instance(); + $product_pos_only = $settings_instance->is_product_pos_only( $product_id ); + $variation_pos_only = $settings_instance->is_variation_pos_only( $product_id ); - if ( $pos_visibility === 'pos_only' ) { + if ( $product_pos_only || $variation_pos_only ) { return false; } @@ -191,9 +160,14 @@ public function prevent_pos_only_add_to_cart( $passed, $product_id ) { * @return void */ public function store_api_prevent_pos_only_add_to_cart( WC_Product $product ) { - $pos_visibility = get_post_meta( $product->get_id(), '_pos_visibility', true ); + $settings_instance = Settings::instance(); + if ( $product->is_type( 'variation' ) ) { + $pos_only = $settings_instance->is_variation_pos_only( $product->get_id() ); + } else { + $pos_only = $settings_instance->is_product_pos_only( $product->get_id() ); + } - if ( $pos_visibility === 'pos_only' ) { + if ( $pos_only ) { throw new NotPurchasableException( 'woocommerce_pos_product_not_purchasable', $product->get_name() diff --git a/includes/Services/Settings.php b/includes/Services/Settings.php index b14e2d1..11fc553 100644 --- a/includes/Services/Settings.php +++ b/includes/Services/Settings.php @@ -73,6 +73,28 @@ class Settings { 'tools' => array( 'use_jwt_as_param' => false, ), + 'visibility' => array( + 'products' => array( + 'default' => array( + 'pos_only' => array( + 'ids' => array(), + ), + 'online_only' => array( + 'ids' => array(), + ), + ), + ), + 'variations' => array( + 'default' => array( + 'pos_only' => array( + 'ids' => array(), + ), + 'online_only' => array( + 'ids' => array(), + ), + ), + ), + ), ); /** @@ -428,6 +450,305 @@ public function get_payment_gateways_settings() { return apply_filters( 'woocommerce_pos_payment_gateways_settings', $response ); } + /** + * POS Visibility settings. + */ + public function get_visibility_settings() { + $default_settings = self::$default_settings['visibility']; + $settings = get_option( self::$db_prefix . 'visibility', array() ); + + // if the key does not exist in db settings, use the default settings + foreach ( $default_settings as $key => $value ) { + if ( ! \array_key_exists( $key, $settings ) ) { + $settings[ $key ] = $value; + } + } + + /* + * Filters the visibility settings. + * + * @param {array} $settings + * @returns {array} $settings + * @since 1.0.0 + * @hook woocommerce_pos_visibility_settings + */ + return apply_filters( 'woocommerce_pos_visibility_settings', $settings ); + } + + /** + * Update visibility settings. + * + * @param array $args The visibility settings to update. + * @return bool|WP_Error True on success, WP_Error on failure. + */ + public function update_visibility_settings( array $args ) { + // Validate and normalize arguments. + if ( empty( $args['post_type'] ) || ! isset( $args['ids'] ) ) { + return new WP_Error( + 'woocommerce_pos_settings_error', + __( 'Invalid arguments provided', 'woocommerce-pos' ), + array( 'status' => 400 ) + ); + } + + $post_type = $args['post_type']; + $scope = $args['scope'] ?? 'default'; + $visibility = $args['visibility'] ?? ''; + $ids = is_array( $args['ids'] ) ? $args['ids'] : array( $args['ids'] ); + $ids = array_filter( array_map( 'intval', $ids ) ); // Force to array of integers. + + // Get the current visibility settings. + $current_settings = $this->get_visibility_settings(); + + // Define the opposite visibility type. + $opposite_visibility = ( $visibility === 'pos_only' ) ? 'online_only' : 'pos_only'; + + // Add or remove IDs based on the visibility type. + foreach ( $ids as $id ) { + if ( $visibility === '' ) { + // Remove from both pos_only and online_only. + $current_settings[ $post_type ][ $scope ]['pos_only']['ids'] = $this->remove_id_from_visibility( + $current_settings[ $post_type ][ $scope ]['pos_only']['ids'], + $id + ); + $current_settings[ $post_type ][ $scope ]['online_only']['ids'] = $this->remove_id_from_visibility( + $current_settings[ $post_type ][ $scope ]['online_only']['ids'], + $id + ); + } else { + // Add to the specified visibility type. + $current_settings[ $post_type ][ $scope ][ $visibility ]['ids'] = $this->add_id_to_visibility( + $current_settings[ $post_type ][ $scope ][ $visibility ]['ids'], + $id + ); + // Remove from the opposite visibility type. + $current_settings[ $post_type ][ $scope ][ $opposite_visibility ]['ids'] = $this->remove_id_from_visibility( + $current_settings[ $post_type ][ $scope ][ $opposite_visibility ]['ids'], + $id + ); + } + } + + return $this->save_settings( 'visibility', $current_settings ); + } + + /** + * Add an ID to a visibility type if it doesn't already exist. + * + * @param array $ids The current array of IDs. + * @param int $id The ID to add. + * @return array The updated array of IDs. + */ + private function add_id_to_visibility( array $ids, int $id ): array { + if ( ! in_array( $id, $ids, true ) ) { + $ids[] = $id; + } + return $ids; + } + + /** + * Remove an ID from a visibility type if it exists. + * + * @param array $ids The current array of IDs. + * @param int $id The ID to remove. + * @return array The updated array of IDs. + */ + private function remove_id_from_visibility( array $ids, int $id ): array { + return array_filter( + $ids, + function ( $existing_id ) use ( $id ) { + return $existing_id !== $id; + } + ); + } + + /** + * Get product visibility settings. + * + * @param string $scope The scope of the settings to get. 'default' or store ID. + * + * @return array $settings The product visibility settings, eg: { pos_only: { ids: [1, 2, 3] }, online_only: { ids: [4, 5, 6] } + */ + public function get_product_visibility_settings( $scope = 'default' ) { + $settings = $this->get_visibility_settings(); + + /* + * Filters the product visibility settings. + * + * @param {array} $settings + * @returns {array} $settings + * @since 1.0.0 + * @hook woocommerce_pos_product_visibility_settings + */ + return apply_filters( 'woocommerce_pos_product_visibility_settings', $settings['products'][ $scope ], $scope ); + } + + /** + * Get product visibility settings. + * + * @param string $scope The scope of the settings to get. 'default' or store ID. + * + * @return array $settings The product visibility settings, eg: { ids: [1, 2, 3] } + */ + public function get_pos_only_product_visibility_settings( $scope = 'default' ) { + $settings = $this->get_product_visibility_settings( $scope ); + + /* + * Filters the product visibility settings. + * + * @param {array} $settings + * @returns {array} $settings + * @since 1.0.0 + * @hook woocommerce_pos_product_visibility_settings + */ + return apply_filters( 'woocommerce_pos_pos_only_product_visibility_settings', $settings['pos_only'], $scope ); + } + + /** + * Get product visibility settings. + * + * @param string $scope The scope of the settings to get. 'default' or store ID. + * + * @return array $settings The product visibility settings, eg: { ids: [1, 2, 3] } + */ + public function get_online_only_product_visibility_settings( $scope = 'default' ) { + $settings = $this->get_product_visibility_settings( $scope ); + + /* + * Filters the product visibility settings. + * + * @param {array} $settings + * @returns {array} $settings + * @since 1.0.0 + * @hook woocommerce_pos_product_visibility_settings + */ + return apply_filters( 'woocommerce_pos_online_only_product_visibility_settings', $settings['online_only'], $scope ); + } + + /** + * Get product visibility settings. + * + * @param string $scope The scope of the settings to get. 'default' or store ID. + * + * @return array $settings The product visibility settings, eg: { pos_only: { ids: [1, 2, 3] }, online_only: { ids: [4, 5, 6] } + */ + public function get_variations_visibility_settings( $scope = 'default' ) { + $settings = $this->get_visibility_settings(); + + /* + * Filters the product visibility settings. + * + * @param {array} $settings + * @returns {array} $settings + * @since 1.0.0 + * @hook woocommerce_pos_product_visibility_settings + */ + return apply_filters( 'woocommerce_pos_variations_visibility_settings', $settings['variations'][ $scope ], $scope ); + } + + /** + * Get product visibility settings. + * + * @param string $scope The scope of the settings to get. 'default' or store ID. + * + * @return array $settings The product visibility settings, eg: { ids: [1, 2, 3] } + */ + public function get_pos_only_variations_visibility_settings( $scope = 'default' ) { + $settings = $this->get_variations_visibility_settings( $scope ); + + /* + * Filters the product visibility settings. + * + * @param {array} $settings + * @returns {array} $settings + * @since 1.0.0 + * @hook woocommerce_pos_product_visibility_settings + */ + return apply_filters( 'woocommerce_pos_pos_only_variations_visibility_settings', $settings['pos_only'], $scope ); + } + + /** + * Get product visibility settings. + * + * @param string $scope The scope of the settings to get. 'default' or store ID. + * + * @return array $settings The product visibility settings, eg: { ids: [1, 2, 3] } + */ + public function get_online_only_variations_visibility_settings( $scope = 'default' ) { + $settings = $this->get_variations_visibility_settings( $scope ); + + /* + * Filters the product visibility settings. + * + * @param {array} $settings + * @returns {array} $settings + * @since 1.0.0 + * @hook woocommerce_pos_product_visibility_settings + */ + return apply_filters( 'woocommerce_pos_online_only_variations_visibility_settings', $settings['online_only'], $scope ); + } + + /** + * Check if a product is POS only. + * + * @param string|int $product_id + * + * @return bool + */ + public function is_product_pos_only( $product_id ) { + $product_id = (int) $product_id; + $settings = $this->get_pos_only_product_visibility_settings(); + $pos_only_ids = array_map( 'intval', (array) $settings['ids'] ); + + return in_array( $product_id, $pos_only_ids, true ); + } + + /** + * Check if a product is Online only. + * + * @param string|int $product_id + * + * @return bool + */ + public function is_product_online_only( $product_id ) { + $product_id = (int) $product_id; + $settings = $this->get_online_only_product_visibility_settings(); + $online_only_ids = array_map( 'intval', (array) $settings['ids'] ); + + return in_array( $product_id, $online_only_ids, true ); + } + + /** + * Check if a variation is POS only. + * + * @param string|int $variation_id + * + * @return bool + */ + public function is_variation_pos_only( $variation_id ) { + $variation_id = (int) $variation_id; + $settings = $this->get_pos_only_variations_visibility_settings(); + $pos_only_ids = array_map( 'intval', (array) $settings['ids'] ); + + return in_array( $variation_id, $pos_only_ids, true ); + } + + /** + * Check if a variation is Online only. + * + * @param string|int $variation_id + * + * @return bool + */ + public function is_variation_online_only( $variation_id ) { + $variation_id = (int) $variation_id; + $settings = $this->get_online_only_variations_visibility_settings(); + $online_only_ids = array_map( 'intval', (array) $settings['ids'] ); + + return in_array( $variation_id, $online_only_ids, true ); + } + + /** * Delete settings in WP options table. * @@ -472,6 +793,6 @@ public static function get_db_version() { * bumps the idb version number. */ public static function bump_versions(): void { - add_option( 'woocommerce_pos_db_version', VERSION, '', 'no' ); + update_option( 'woocommerce_pos_db_version', VERSION ); } } diff --git a/includes/WC_API.php b/includes/WC_API.php index de19077..000782b 100644 --- a/includes/WC_API.php +++ b/includes/WC_API.php @@ -10,10 +10,26 @@ namespace WCPOS\WooCommercePOS; +use WP_Query; +use WCPOS\WooCommercePOS\Services\Settings; + /** * */ class WC_API { + /** + * Indicates if the current request is for WooCommerce products. + * + * @var bool + */ + private $is_woocommerce_rest_api_products_request = false; + + /** + * Indicates if the current request is for WooCommerce variations. + * + * @var bool + */ + private $is_woocommerce_rest_api_variations_request = false; /** * @@ -22,44 +38,61 @@ public function __construct() { $pos_only_products = woocommerce_pos_get_settings( 'general', 'pos_only_products' ); if ( $pos_only_products ) { - add_filter( 'woocommerce_rest_product_object_query', array( $this, 'hide_pos_only_products' ), 10, 2 ); - add_filter( 'woocommerce_rest_product_variation_object_query', array( $this, 'hide_pos_only_products' ), 10, 2 ); + add_filter( 'rest_pre_dispatch', array( $this, 'set_woocommerce_rest_api_request_flags' ), 10, 3 ); + add_filter( 'posts_where', array( $this, 'exclude_pos_only_products_from_api_response' ), 10, 2 ); } } /** - * Filter the query arguments for a request. * - * Enables adding extra arguments or setting defaults for a post - * collection request. + */ + public function set_woocommerce_rest_api_request_flags( $result, $server, $request ) { + $route = $request->get_route(); + + if ( strpos( $route, '/wc/v3/products' ) === 0 || strpos( $route, '/wc/v2/products' ) === 0 || strpos( $route, '/wc/v1/products' ) === 0 ) { + $this->is_woocommerce_rest_api_products_request = true; + + if ( strpos( $route, '/variations' ) !== false ) { + $this->is_woocommerce_rest_api_variations_request = true; + } + } + + return $result; + } + + /** + * Hide POS only products from the API response. * - * @param array $args Key value array of query var to query value. - * @param WP_REST_Request $request The request used. + * @param string $where The WHERE clause of the query. + * @param WP_Query $query The WP_Query instance (passed by reference). + * + * @return string */ - public function hide_pos_only_products( $args, $request ) { - $meta_query = array( - 'relation' => 'OR', - array( - 'key' => '_pos_visibility', - 'compare' => 'NOT EXISTS', - ), - array( - 'key' => '_pos_visibility', - 'value' => 'pos_only', - 'compare' => '!=', - ), - ); - - if ( empty( $args['meta_query'] ) ) { - $args['meta_query'] = $meta_query; - } else { - $args['meta_query'] = array( - 'relation' => 'AND', - $args['meta_query'], - $meta_query, - ); + public function exclude_pos_only_products_from_api_response( $where, $query ) { + global $wpdb; + $settings_instance = Settings::instance(); + + // Hide POS only variations from the API response. + if ( ! $this->is_woocommerce_rest_api_variations_request ) { + $settings = $settings_instance->get_pos_only_variations_visibility_settings(); + + if ( isset( $settings['ids'] ) && ! empty( $settings['ids'] ) ) { + $exclude_ids = array_map( 'intval', (array) $settings['ids'] ); + $ids_format = implode( ',', array_fill( 0, count( $exclude_ids ), '%d' ) ); + $where .= $wpdb->prepare( " AND {$wpdb->posts}.ID NOT IN ($ids_format)", $exclude_ids ); + } + + // Hide POS only products from the API response. + } elseif ( $this->is_woocommerce_rest_api_products_request ) { + $settings = $settings_instance->get_pos_only_product_visibility_settings(); + + if ( isset( $settings['ids'] ) && ! empty( $settings['ids'] ) ) { + $exclude_ids = array_map( 'intval', (array) $settings['ids'] ); + $ids_format = implode( ',', array_fill( 0, count( $exclude_ids ), '%d' ) ); + $where .= $wpdb->prepare( " AND {$wpdb->posts}.ID NOT IN ($ids_format)", $exclude_ids ); + } } - return $args; + return $where; } } diff --git a/includes/updates/update-1.6.1.php b/includes/updates/update-1.6.1.php new file mode 100644 index 0000000..82f2272 --- /dev/null +++ b/includes/updates/update-1.6.1.php @@ -0,0 +1,99 @@ +get_col( + " + SELECT post_id + FROM {$wpdb->postmeta} + WHERE meta_key = '_pos_visibility' AND meta_value = 'pos_only' AND post_id IN (SELECT ID FROM {$wpdb->posts} WHERE post_type = 'product') + " + ); + + // Query to get all products with _pos_visibility = 'online_only'. + $online_only_product_ids = $wpdb->get_col( + " + SELECT post_id + FROM {$wpdb->postmeta} + WHERE meta_key = '_pos_visibility' AND meta_value = 'online_only' AND post_id IN (SELECT ID FROM {$wpdb->posts} WHERE post_type = 'product') + " + ); + + // Query to get all variations with _pos_visibility = 'pos_only'. + $pos_only_variation_ids = $wpdb->get_col( + " + SELECT post_id + FROM {$wpdb->postmeta} + WHERE meta_key = '_pos_visibility' AND meta_value = 'pos_only' AND post_id IN (SELECT ID FROM {$wpdb->posts} WHERE post_type = 'product_variation') + " + ); + + // Query to get all variations with _pos_visibility = 'online_only'. + $online_only_variation_ids = $wpdb->get_col( + " + SELECT post_id + FROM {$wpdb->postmeta} + WHERE meta_key = '_pos_visibility' AND meta_value = 'online_only' AND post_id IN (SELECT ID FROM {$wpdb->posts} WHERE post_type = 'product_variation') + " + ); + + // Prepare the new visibility settings. + $new_visibility_settings = array( + 'products' => array( + 'default' => array( + 'pos_only' => array( + 'ids' => array_map( 'intval', $pos_only_product_ids ), + ), + 'online_only' => array( + 'ids' => array_map( 'intval', $online_only_product_ids ), + ), + ), + ), + 'variations' => array( + 'default' => array( + 'pos_only' => array( + 'ids' => array_map( 'intval', $pos_only_variation_ids ), + ), + 'online_only' => array( + 'ids' => array_map( 'intval', $online_only_variation_ids ), + ), + ), + ), + ); + + // Update the option in the database. + update_option( $option_key, $new_visibility_settings, false ); +} + +// Run the update script. +woocommerce_pos_update_pos_visibility_settings_to_1_6_1(); diff --git a/package.json b/package.json index c728cc8..3eca2f5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@wcpos/woocommerce-pos", - "version": "1.6.0", + "version": "1.6.1", "description": "A simple front-end for taking WooCommerce orders at the Point of Sale.", "main": "index.js", "workspaces": { diff --git a/readme.txt b/readme.txt index b285fa9..21d93db 100644 --- a/readme.txt +++ b/readme.txt @@ -80,6 +80,10 @@ There is more information on our website at [https://wcpos.com](https://wcpos.co == Changelog == += 1.6.1 - 2024/06/17 = +- **Enhancement:** Changed the way POS Only and Online Only products are managed. + - Moved from using postmeta (`_pos_visibility`) to using centralized settings in the options table (`woocommerce_pos_settings_visibility`). + = 1.6.0 - 2024/06/12 = * Improved: Performance for large stores * Added: Log screen for insights into the POS performance and events diff --git a/woocommerce-pos.php b/woocommerce-pos.php index a8f95cb..d99a061 100644 --- a/woocommerce-pos.php +++ b/woocommerce-pos.php @@ -3,7 +3,7 @@ * Plugin Name: WooCommerce POS * Plugin URI: https://wordpress.org/plugins/woocommerce-pos/ * Description: A simple front-end for taking WooCommerce orders at the Point of Sale. Requires WooCommerce. - * Version: 1.6.0 + * Version: 1.6.1 * Author: kilbot * Author URI: http://wcpos.com * Text Domain: woocommerce-pos @@ -24,7 +24,7 @@ namespace WCPOS\WooCommercePOS; // Define plugin constants. -const VERSION = '1.6.0'; +const VERSION = '1.6.1'; const PLUGIN_NAME = 'woocommerce-pos'; const SHORT_NAME = 'wcpos'; \define( __NAMESPACE__ . '\PLUGIN_FILE', plugin_basename( __FILE__ ) ); // 'woocommerce-pos/woocommerce-pos.php'