Salesforce CPQ – Product Search Plugin – Extended Example

The Background

This is a quick review of a scenario posed by Tejas Kapasi in response to the last post on the basics of Product Search Plugin. He questioned whether or not the Product Search Plugin could be employed to prevent adding a Product (Bundle in this example, but would apply to any Product) that had already been added to the Quote. He is today using the QCP in a pretty clever way as illustrated in his detailed post here. Let’s take a look at the example of doing something similar using the Product Search Plugin as well. Make sure to check the ‘Notes & Considerations‘ section at the bottom for important disclaimers and things to think about.

Scenario Review

We have our same scenario, but will add a Version 2 of the Gaming Laptop as well, so that we have some additional items to add and test with. We will add a check to the Product Search Plugin to query the existing bundle Quote Lines on the Quote, and update the getAdditionalSuggestFilters to exclude Products already included on the Quote. This is to extend our existing example, and the same approach should work for getAdditionalSearchFilters or full search/suggest methods as well, with minor modification of course. Before we make any modifications to teh Product Search Plugin, our Guided Selling for Gaming Laptop leads to two selections now – Gaming Laptop and Gaming Laptop Version 2.

Update Product Search Plugin

As stated, we will again employ getAdditionalSuggestFilters to extend our Guided Selling filters. The approach we will use is to query the Quote Lines against our Quote for any Bundle headers. If found, we’ll build a set and pass into the additional filter to check that Product2.Id is NOT IN that set.

    global String getAdditionalSuggestFilters(SObject quote, Map<String,Object> fieldValuesMap){
        Set<Id> setBundleId = new Set<Id>();
        String additionalFilter = NULL;
        
        //Suggest to use custom metadata here to dictate behavior so that it is more easily maintainble.
        
        //Query for existing Quote Lines where Bundle and get ProductId
        for(SBQQ__QuoteLine__c ql : [SELECT Id, SBQQ__Product__c FROM SBQQ__QuoteLine__c 
                                     WHERE SBQQ__RequiredBy__c = null AND SBQQ__Quote__c = :quote.Id]){
            setBundleId.add(ql.SBQQ__Product__c);
        }
        
        if(setBundleId.size()>0){
            //Turn to string for dynamic soql
        	String bundleFilter = inClausify(setBundleId);
            additionalFilter = 'Product2.Id NOT IN '+ bundleFilter;
        }
        
		return additionalFilter;
	}

The additionalFilter string returned is eventually being applied by CPQ package class method (SBQQ.PricebookEntryDAO.loadByQuoteProces), and trying to pass the set as a variable in the string directly leads to error. Therefore, I have applied this handy ‘inClausify’ method as given by Robert Sösemann in this StackExchange post. This allows us to build the additionalFilter easily.

    private String inClausify(Set<Id> ids) {
        String inClause = String.format( '(\'\'{0}\'\')', 
                             new List<String> { String.join( new List<Id>(ids) , '\',\'') });
        return inClause;
    }

Please also refer to the Notes & Considerations section as I think gating this filtering behavior using custom metadata (or similar approach) would be useful for maintainability purposes.

Testing It Out

On our initial entry into the Product Search, we fill out our Guided Selling with Gaming Laptop in the same way as in previous posts. We are presented with both versions of the Laptop as no Quote Lines have yet been added, so the additionalFilter is being passed as the default NULL.

Let’s add the regular version to the Quote, and Save. Again, see Notes & Considerations as the save is required here due to querying the Quote Lines to build the filter.

After Quick Save, we go back into Add Products, and we’ll now see that only the Version 2 laptop is available. This is because now we have built the additionalFilter based on existing Quote Lines to exclude the regular version which was already added to the Quote.

Add the Version 2 laptop to the Quote and Quick Save. Now if we go back to Add Products again, we don’t see any available selections because both versions were already added to the Quote. As you can see in the debug log, the additionalFilter works for single and multiple returns as we’d expect.

Great, so this is behaving as we’d expect, and allows us to build more advanced filtering logic based on Quote and Quote Line attributes. Remember the previous video from TrailheadDX offering previous Order Product records from the Account on the Quote as a sorting mechanism using this tool. Clearly there are many options like this for further extending the search and filter behaviors, and now just a matter of creativity!

Notes & Considerations

  • Reliant on Quote Lines being committed prior to adding/search for Products. Otherwise, the Quote Lines are not available in the query and the additional filter will not be filled.
  • Clone bundle in the QLE will still allow duplicate addition
  • Would be useful to implement a controlling mechanism such as Custom Metadata to gate the different behaviors in the methods. For example, Custom Metadata Type ‘PSP_Behavior__mdt’ with a picklist for Quote Type and a checkbox for ‘Prevent Duplicates’. Then in the Product Search Plugin, query and check custom metadata for that Quote Type, and if Prevent Duplicates is true then add the additional filter. I would be concerned with putting a bunch of logic in these Plugins without the ability to control the behavior from outside the class.
  • Such an approach should be considered more of a guidance mechanism for Product addition (preventing duplicate product through search) than a foolproof way to prevent duplicates. Using the QCP previously mentioned, or some clever way using the CPQ declarative approaches, may be a preferable way to actually block outright.

Thanks for reading this amendment post and thanks to Tejas Kapasi for the question that prompted further investigation!

One comment

Leave a Reply

Your email address will not be published. Required fields are marked *