Search code examples
phpwordpresswoocommercecartcheckout

Make a product to be purchased only once or not if any item from a specific product category has been purchased in Woocommerce


In woocommerce, I am just using simple products and I have a special product "free trial package" from a product category "A", which I'm willing to:

  1. let the customer only purchase this "free trial package" once
  2. if the customer buy other products from the same product category "A" first, then the customer can't purchase this "free trial package" anymore

This means the customer only be able to get the "free trial package" only if it's the first purchase of that product category "A".

My code doesn't seem to work properly and it only do the 1st point.

The other problem is that if the user order finishes, next time user open cart it shows a weird message: "you cant select this product anymore, you already bought this package".It's like Woocommerce cache problem or something…

Here is my code so far:

function sv_disable_repeat_purchase( $purchasable, $product ) {
    $non_purchasable = 190;

    $product_id = $product->is_type( 'variation' ) ? $product->variation_id : $product->id;

    if ( $product_id != $non_purchasable ) {
        $purchasable = true;
    }

    if ( wc_customer_bought_product( wp_get_current_user()->user_email, get_current_user_id(), $product_id ) ) {
        $purchasable = false;
    }

    if ( $purchasable && $product->is_type( 'variation' ) ) {
        $purchasable = $product->parent->is_purchasable();
    }

    return $purchasable;
}
add_filter( 'woocommerce_variation_is_purchasable', 'sv_disable_repeat_purchase', 10, 2 );
add_filter( 'woocommerce_is_purchasable', 'sv_disable_repeat_purchase', 10, 2 );

Any help is appreciated.


Solution

  • Update 2

    Based on "Check if a customer has purchased a specific products in WooCommerce" answer code, the following will prevent purchases from a "trial package" product, if it has already been purchased once, or if any product for a defined product category "A" has been already purchased.

    Your actual code is outdated since Woocommerce 3 and there some mistakes…

    I use a custom conditional function inspired from wc_customer_bought_product() source code, to check if customer can buy the "free trial" product. This function will check as well for a product ID or for a product category:

    function has_bought_items( $user_id = 0, $product_ids = 0, $categories_slugs = '' ) {
        global $wpdb;
        $customer_id = $user_id == 0 || $user_id == '' ? get_current_user_id() : $user_id;
        $statuses    = array_map( 'esc_sql', wc_get_is_paid_statuses() );
    
        if ( is_array( $product_ids ) ) {
            $product_ids = implode(',', $product_ids);
        }
    
        if ( $product_ids !=  ( 0 || '' ) ) {
            $query_in = "IN ($product_ids)";
        }
        else {
            $categories = is_array( $categories_slugs ) ? implode("','", $categories_slugs) : $categories_slugs;
    
            $query_in = "IN (
            SELECT DISTINCT products.ID  FROM {$wpdb->prefix}posts AS products
            INNER JOIN {$wpdb->prefix}term_relationships as term_rel
                ON products.ID = term_rel.object_id
            INNER JOIN {$wpdb->prefix}term_taxonomy as term_tax
                ON term_rel.term_taxonomy_id = term_tax.term_taxonomy_id
            INNER JOIN {$wpdb->prefix}terms as terms
                ON term_tax.term_taxonomy_id = terms.term_id
            WHERE products.post_status = 'publish'
            AND term_tax.taxonomy = 'product_cat'
            AND terms.slug IN ('$categories')
            )";
        }
    
        // Count the number of products
        $product_count_query = $wpdb->get_var( "
            SELECT DISTINCT COUNT(orders.ID)
            FROM {$wpdb->prefix}posts AS orders
            INNER JOIN {$wpdb->prefix}postmeta AS order_meta
                ON orders.ID = order_meta.post_id
            INNER JOIN {$wpdb->prefix}woocommerce_order_items AS order_items
                ON orders.ID = order_items.order_id
            INNER JOIN {$wpdb->prefix}woocommerce_order_itemmeta AS order_itemmeta
                ON order_items.order_item_id = order_itemmeta.order_item_id
            WHERE orders.post_status IN ( 'wc-" . implode( "','wc-", $statuses ) . "' )
            AND order_meta.meta_key = '_customer_user'
            AND order_meta.meta_value = $customer_id
            AND order_itemmeta.meta_key IN ( '_product_id', '_variation_id' )
            AND order_itemmeta.meta_value $query_in
        " );
    
        // Return a boolean value if count is higher than 0
        return $product_count_query > 0 ? true : false;
    }
    

    Your revisited code using the custom conditional function above:

    // add_filter( 'woocommerce_variation_is_purchasable', 'trial_package_purchasable_once', 10, 2 );
    add_filter( 'woocommerce_is_purchasable', 'trial_package_purchasable_once', 10, 2 );
    function trial_package_purchasable_once( $purchasable, $product ) {
        // HERE set the free trial product Id to be purchased only once (can be an array of Ids too)
        $free_trial_id = 50;
    
        // HERE set the specific product category SLUG (could be an array of slugs too)
        $categories = array('lettering'); // Only slug terms
    
            if ( $product->get_id() == $free_trial_id ) {
                // Check if any item of the specific product category has already been purchased once
                if ( has_bought_items( get_current_user_id(), '', $categories ) ) {
                    return false;
                }
                // Check if the free trial product has already been purchased once
                elseif ( has_bought_items( get_current_user_id(), $free_trial_id ) ) {
                    return false;
                }
            }
        }
    
        return $purchasable;
    }
    

    Code goes in function.php file of your active child theme (or active theme). Tested and works.


    To handle product categories Term IDs instead of Term Slugs use the following conditional function instead:

    // For Product category(ies) Id(s)
    function has_bought_items( $user_id = 0, $product_ids = 0, $categories_ids = '' ) {
        global $wpdb;
        $customer_id = $user_id == 0 || $user_id == '' ? get_current_user_id() : $user_id;
        $statuses    = array_map( 'esc_sql', wc_get_is_paid_statuses() );
    
        if ( is_array( $product_ids ) ) {
            $product_ids = implode(',', $product_ids);
        }
    
        if ( $product_ids !=  ( 0 || '' ) ) {
            $query_in = "IN ($product_ids)";
        }
        else {
            $categories = is_array( $categories_ids ) ? implode(',', $categories_ids) : $categories_ids;
    
            $query_in = "IN (
            SELECT DISTINCT products.ID  FROM {$wpdb->prefix}posts AS products
            INNER JOIN {$wpdb->prefix}term_relationships as term_rel
                ON products.ID = term_rel.object_id
            INNER JOIN {$wpdb->prefix}term_taxonomy as term_tax
                ON term_rel.term_taxonomy_id = term_tax.term_taxonomy_id
            INNER JOIN {$wpdb->prefix}terms as terms
                ON term_tax.term_taxonomy_id = terms.term_id
            WHERE products.post_status = 'publish'
            AND term_tax.taxonomy = 'product_cat'
            AND terms.term_id IN ($categories)
            )";
        }
    
        // Count the number of products
        $product_count_query = $wpdb->get_var( "
            SELECT DISTINCT COUNT(orders.ID)
            FROM {$wpdb->prefix}posts AS orders
            INNER JOIN {$wpdb->prefix}postmeta AS order_meta
                ON orders.ID = order_meta.post_id
            INNER JOIN {$wpdb->prefix}woocommerce_order_items AS order_items
                ON orders.ID = order_items.order_id
            INNER JOIN {$wpdb->prefix}woocommerce_order_itemmeta AS order_itemmeta
                ON order_items.order_item_id = order_itemmeta.order_item_id
            WHERE orders.post_status IN ( 'wc-" . implode( "','wc-", $statuses ) . "' )
            AND order_meta.meta_key = '_customer_user'
            AND order_meta.meta_value = $customer_id
            AND order_itemmeta.meta_key IN ( '_product_id', '_variation_id' )
            AND order_itemmeta.meta_value $query_in
        " );
    
        // Return a boolean value if count is higher than 0
        return $product_count_query > 0 ? true : false;
    }
    

    It's better alternative for foreign languages with special characters.