How to create a $1M WooCommerce Composite Product

12 December 2024

WooCommerce is a powerful tool to provide different product types for your online shop. However, by default it can’t cover something more complex than a variable product with a few properties to select. Sometimes that’s necessary to provide a possibility to build an own configured kit from multiple smaller products and custom fields. Moreover, that might be necessary to mix and match products to make them compatible.

Here is a real story about the implementation of such a complex product made using the WooCommerce Products Wizard and WooCommerce Step Filter plugins.

WooCommerce Products Wizard

WooCommerce Products Wizard

Composite Product Configurator & Builder
Learn more

Basic configurator

It was started from a presale request about the possibility to create a simple step-by-step wizard to select and combine different bike parts. A pretty usual case to be implemented with Products Wizard. Already hundreds of projects are created using it.

Firstly, we implemented a simple few steps Products Wizard: Welcome – Category 1 – Category 2 … Cart. That made it possible to go through these steps and find all required products and add them to the cart as a single bundle. That was the starting point.

So the initial wizard workflow was found. However, the bottleneck of the site is its products. Most of them are made as variation ones and have a ton of different technical properties to select, which might be complicated for most users or could lead to mistakes even for the advanced users.

But then the situation went in a very different direction. We got this message from the site owner:

I’m looking for someone who would be able to code our digital sales assistant as in asking questions to the customer and then based on his answers add products to the cart.

That was necessary to create some tool to mix-and-match a lot of technically-complex products. Let’s dive in.

Extending the project possibilities

Each step of the wizard represents one specific category of products, like motor or battery. However, there are a lot of products on the site with their own specific and different technical properties, which makes them harder to choose and combine. That was necessary to find different bike parts by the model, power watts, voltage, and so on.

Firstly, the native wizard product filters were used to solve that issue. This is possible to add a category, attribute, or other usual filters. That did the trick to filter and find only appropriate products of a step.

I would like to combine two or more product step-filters. Some filter need to work globally (to ensure compatibility between the products) and some just locally within the tab.

We started to look for a solution to not oblige the client to apply the same filters for every new step opened, because first question answers should be the same among all other steps and categories.

Wizard supports sharing of the filters from one step to another. However, there are some limitations, namely all filters of the source step will be reflected in the target one. You can add extra ones only into the target one. Also, they can’t provide a lot of conditional logic to make the filtering process easier.

A kit consists of many products: a motor, a controller, a display, sensors, etc.

I would like to use the step-filter plugin to let the customer filter its proper kit. We would like to build a complete guide helping him to get all the correct and necessary products in the shopping cart. That being said, I would need a functionality that allows going from one product filter (e.g., motor) to the next product filter (e.g., controller) without forgetting some of the filters set for the previous component. For example, if we let the customer start by configuring the motor, and he picks 36V and 250W, these filters should not be forgotten in the next step, the configuration of the controller.

WooCommerce Step Filter

WooCommerce Step Filter was a good candidate to implement that scenario. It’s made to output filters as step-by-step questions and then apply the answers to find only appropriate products.

Also, it’s possible to add any conditional logic to show or hide steps, questions, or their values according to the user selection. Moreover, it was already possible to pass an answer from one filter to another, which was critical for the project.

But the Step Filter plugin was a separate plugin with no 3rd-party integrations.

WooCommerce Step Filter

WooCommerce Step Filter

Product Filter for WooCommerce
Learn more

So we started to consider making it possible to use Step Filter within a Products Wizard step. That would make the Products Wizard process much easier. Also, that was a puzzling and interesting point to implement. We started to work with that concept.

As Step Filter allows outputting result products right within itself as a separate step, that gives an opportunity to combine the plugins.

Both of the plugins have all handlers and methods to control their behavior. Also, they are ready to be launched with any environment as a different page position or using AJAX calls. This advantage gave a possibility to call a Step Filter on a Product Wizard step and apply the filtering only to the step’s products.

That was the most complicated part of this project. That was necessary to replace the default Products Wizard output with the Step Filter form, but at the same time, the Step Filter should know what products can be requested and shown on that specific wizard step.

Don’t forget: Products Wizard steps can have their own native filters. So that should be possible to extra filter the result products.

After a few loops of coding and deep testing, we finally found a way to combine these plugins and make them work solidly!

The only visible change that was added in the admin part is the setting to select a Step Filter for a wizard’s step when both plugins are active:

And what a fantastic result we have! One more big advantage is the use of the same CSS framework (Bootstrap 5) and the same possibilities to restyle output in both plugins the same way.

We made the filter and wizard look more organic to the site brand and fit to each other too.


So good so far!

Then we started to improve users experience for this complex and specific project.

Replace variable products by single variations

The first feature request was about output product variables as independent products to make the variation selection process much easier for the customer.

Is it somehow possible to display a single variation as the final result? Right now it is displaying the whole variable product, so the user would have to go through all the dropdowns again to get its desired product. Ideally, it would show a thumbnail and the price of the variation meeting all attributes.

Not a complicated point at a glance. We just added a couple of argument filters to the site. These filters make the wizard request only simple products and product variations as simple products, excluding variable products.

// modify products query
add_filter('wcpw_step_products_query_args', 'queryOnlySimpleProductsAndVariations');
add_filter('wcsf_query_args_by_filter', 'queryOnlySimpleProductsAndVariations');
add_filter('wcsf_preliminary_results_query_args', 'queryOnlySimpleProductsAndVariations');
add_filter('wcsf_products_query_args', 'queryOnlySimpleProductsAndVariations');

function queryOnlySimpleProductsAndVariations($output) {
    if (!isset($output['tax_query']['variations_and_simple'])) {
        $output['post_type'] = ['product', 'product_variation'];
        $output['tax_query']['variations_and_simple'] = [
            'taxonomy' => 'product_type',
            'field' => 'slug',
            'terms' => 'variable',
            'operator' => 'NOT IN'
        ];
    }

    return $output;
}

add_filter('wcpw_products_request_args', function ($output) {
    $output['queryArgs'] = queryOnlySimpleProductsAndVariations($output['queryArgs']);

    return $output;
});

But we found an issue using that approach. It was about the wizard requests by product attributes and categories. Using such requests, only simple products were found. The reason for that issue is because WooCommerce doesn’t attach product variations to their parent product attributes and categories. So they just can’t be found by these terms.

We decided to reflect product terms as attributes and categories into all their variations while the product save action. Here is the extra code we added to the site:

function reflectProductAttributesIntoVariations($productId) {
    $product = wc_get_product($productId);

    if ($product->get_type() != 'variable') {
        return;
    }

    $attributes = array_filter($product->get_attributes(), 'wc_attributes_array_filter_visible');

    foreach ($attributes as $attribute) {
        if (!$attribute instanceof \WC_Product_Attribute || !$attribute->is_taxonomy()) {
            continue;
        }

        $terms = [];

        // pass slugs cause IDs might be used as strings
        foreach ($attribute->get_options() as $termId) {
            $terms[] = get_term($termId, $attribute->get_taxonomy())->slug;
        }

        foreach ($product->get_children() as $variationId) {
            $variation = wc_get_product($variationId);

            foreach ($variation->get_attributes() as $variationAttribute => $value) {
                if ($value && taxonomy_exists($variationAttribute)) {
                    wp_set_object_terms((int) $variationId, $value, $variationAttribute);
                } elseif ($attribute->get_taxonomy() == $variationAttribute) {
                    wp_set_object_terms((int) $variationId, $terms, $attribute->get_taxonomy());
                }
            }

            wp_set_object_terms((int) $variationId, $terms, $attribute->get_taxonomy());
        }
    }

    // pass categories
    foreach ($product->get_children() as $variationId) {
        wp_set_object_terms((int) $variationId, $product->get_category_ids(), 'product_cat');
    }
}

// save product action
add_action('save_post_product', function ($productId) {
    if ((defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) || !current_user_can('edit_post', $productId)) {
        return;
    }

    reflectProductAttributesIntoVariations($productId);
});

And the issue was solved fancy 🙂 Let’s going further.

Dynamic discount by total amount

One more pretty popular request was about applying a discount according to the product’s number in the cart.

We would like to have a price discount feature, something very simple, that reduces the total price for the customer depending on his cart value.

Fortunately, that was very easy to solve. Wizard already had a setting to provide a percentage discount to the products bought using it. However, by default, it can have only a fixed percent. To not waste too much time, we added custom code to modify this setting value dynamically according to the number of the products in the wizard cart.

// dynamic discount value by the current cart count
add_filter('wcpw_post_setting', function ($value, $id, $setting) {
    if ($setting != 'price_discount' || (is_admin() && !wp_doing_ajax())) {
        return $value;
    }

    $cartCount = 0;
    $rules = [
        1 => 0,
        5 => 4,
        7 => 8,
        9 => 10
    ];

    foreach (\WCProductsWizard\Cart::get($id) as $item) {
        if (empty($item['quantity'])) {
            continue;
        }

        $cartCount += $item['quantity'];
    }

    if ($cartCount) {
        foreach ($rules as $count => $discount) {
            if ($cartCount >= $count) {
                $value = $discount;
            } else {
                break;
            }
        }
    }

    return $value;
}, 10, 3);

For that specific case, 5 products give a 4% discount, 7 products make 8%, and 9+ products set the max 10% discount. Also, the code considers the quantity of the products added to the cart.

Done again! What’s next?

Controller – Battery – Adapter

One more significant feature request:

We would like to recommend the correct adapter to our customers, who buy a controller and a battery. In some cases the controller has different connectors than the battery. So they need an adapter.

So the controller has the attribute “controller-battery connector: EC5” and the battery has the attribute “battery-controller connector: XT60” and the corresponding adapter has both matching attributes “controller-battery connector: EC5” & “battery-controller connector: XT60”. So I could implement a product wizard step “Adapter” that is hidden until both attributes are in the cart. Then I would just need a code snippet that presets the filters accordingly: if products with these attributes are in the cart, show products from the category adapter with the matching attributes.

That’s a pretty popular case and had a solution in the wizard for a long time ago. The structure of the database and products was perfect to achieve that.

There is the Availability rules setting for all wizard entities to control the conditional logic of their appearance within the wizard. It also works with the product attributes and checks if some of them are in or not in the cart:

So what’s needed to do to achieve such behavior? First of all, it’s necessary to make a product attribute be available only if it is already in the cart. There are Availability rules setting for every product attribute value. For example, that’s needed to make the attribute “Frame color: Black” depend on itself and should be in the cart to be available.

But how can a product with that attribute be added to the cart if it depends on itself? Easy! These availability rules will not be applied until we force the wizard to check them. To do that, just list the filtered attributes into the Attributes for the using setting of a step where you want to filter the products. At that step the wizard will check each of the listed attributes and their rules, so if a product from a previous step has the same attribute and is in the cart, the attribute is met, and other products having it will be shown too.

That point was successfully solved without extra coding.

Sharing the configurator

Another workflow-improvement request was about sharing the state of the wizard. There were sharing buttons for both the wizard and step filter, and they were implemented via the URL query arguments. But we didn’t run their work together; it can be completely wrong.

When I share my configurations with my customer, they sometimes want to change it and go back in the wizard, clicking on previous steps, but it completely loses the answers of the step filter. That leads to wrong products in the result.

Is it possible that when I share a link, it “remembers” ALL of the answers, also from a step filter and the bonded questions?

The issue mostly depends on the step filter, because initially it wasn’t planned to output a few filters per page at once. So we had to expand its API to let the wizard control and store a few filters.

And that was successfully done! Now the wizard shares its own state, also as all filters used within its steps.

Shorter links

An unpredicted issue with that solution was met: as the wizard has a lot of data, such as step-filters and many products to select, the share link becomes pretty long! Maybe even too long. But these links had to be used for the marketing process, and they hardly could be fine for that case with that shape.

All issues have to be solved! The business knows better what they want, and the programmers can make it real.

To solve that specific case, a new setting was added to the WooCommerce Products Wizard plugin. It’s called the “Share” button type and provides two ways to behave:

  • Long link with data stored in URL;
  • Short link with data stored in DB.

The new workflow just works with the same long URL links and converts them into an MD5 hash. That means a few same share URLs will have the same hash and not pollute the DB with excess data. And while you’re opening a short share link, you’ll be redirected to the long alias.

A new plugin section was added to manage these links:

It allows you to request links for a specific wizard and for specific date ranges. You also can find when a link was created and re-created (modified), as well as the number of clicks of a link.

Smaller workflow improvements

Hide filter question

As we made step-filters have the same answers among the wizard steps, it wasn’t necessary to hide the same already answered ones on the further wizard steps. Mostly, as users can be confused or even change the filter answers, this will lead to wrong product compatibility results.

So that would be better to make these questions hidden but keep them influencing the result.

✅ Done by the new step-filter setting!

Hide step if no products available

With a lot of product filters applied, there might be a situation when no compatible products are available to select on one of the steps. In this case, there would be a confusing empty step with the only “No products to show” message.

To avoid such a situation and make the process more clear, we added a wizard setting to hide a step if there are no products left to show. That situation can be met on any other project, so that setting is built into the Products Wizard plugin.

✅ Done by the new wizard setting!

Control number of products

To not let users be confused with a lot of results, it was necessary to reduce the number of products to be available within the step filters and wizard steps. And don’t forget about the results PDF file, which should have the same product number!

✅ Done by the new wizard and step-filter settings!

Small front-end improvements

And we solved a couple of easier front-side issues. That was much easier.

The first one was about deselecting the wizard products with radio inputs:

I am trying to implement a possibility for the user to “deselect” a product, which is not possible currently because of the nature of radio buttons, which can’t be deselected.

It might look a bit strange according to an HTML form, because mostly we assume one of the radio inputs is always on. However, Products Wizard can handle such situations with min/max product-selected settings.

So we added a pinch of JavaScript code to achieve that behavior:

jQuery(document).on('click', '[data-component~="wcpw-product-choose"]', function () {
    if (this.previousChecked) {
        if (this.checked && this.previousChecked == this.getAttribute('value')) {
            this.checked = false;
            this.previousChecked = null;
        }
    } else {
        for (var element of document.querySelectorAll('[name="' + this.getAttribute('name') + '"]')) {
            element.previousChecked = this.getAttribute('value');
        }
    }
});

jQuery(document).on('change', '[data-component~="wcpw-product-choose"]', function () {
    for (var element of document.querySelectorAll('[name="' + this.getAttribute('name') + '"]')) {
        element.previousChecked = this.getAttribute('value');
        element.haveChanged = true;
    }
});

✅ Pretty simple but works!

Another issue was about the strict workflow of each wizard and filter step:

I don’t want to give the customer the possibility to jump steps; I want him to go through each single step, step by step.

It’s possible to oblige the user to select the required number of products using the min/max products selected wizard setting. The only problem was with the wizard “Next” button, because it was looking available to click even until Step Filter wasn’t finished yet.

We decided to solve it the simplest way with a bit of extra modern CSS, what checks are there any Step Filter on the Products Wizard step currently? And if it is, but not showing the results page, just disable Product Wizard controls:

.woocommerce-products-wizard:has(.wcsf-form:not(.is-step-results)) .woocommerce-products-wizard-control {
    opacity: 0.5;
    pointer-events: none;
}

✅ Done! CSS is so powerful nowadays.

WooCommerce Products Wizard

WooCommerce Products Wizard

Composite Product Configurator & Builder
Learn more

Conclusions

You might say, “So many words in this article! Can we already take a look at this project and try its workflow ourselves?”

Sure you can! The project successfully works and solves the owner’s business process perfectly already for a few years!

You can take a look at it here (published with the consent of the site owner):

windmeile.com/umbausatzkonfigurator

What’s the best benefit of this story? Answer: Most of these features were added to these plugins. That way you already can use them to create such complex WooCommerce composite products too!

And don’t hesitate to message us in case you have some good ideas or feature requests. Maybe you’ll have them implemented in our plugins and your projects 😉

WooCommerce Step Filter

WooCommerce Step Filter

Product Filter for WooCommerce
Learn more
RUB USD