Subscription Conversion – One per unit

You know how on the Product record, you can set up a Product to convert to an Asset? And you can either make it convert to an Asset One per Quote Line or One per unit? One per unit means a Quote Line with a Quantity of 5 gets 5 separate Asset records.

How great! But. What about Subscriptions? You can’t do this with Subscriptions! Gah. Not good. What if I want to convert a Subscription Product to a Subscription record, One per unit, just like Assets can be?

Well! you’ve come to the right place. 😃 Below is a solution to this requirement. It’s a couple Custom Fields, a small Apex Class, and a Process Builder. This solution works with all of the out of the box CPQ functionality. Contracts, Amendments, Renewals, MDQ, you name it. The flow is as follows.

  • Create Quote with a line of a Subscription Product that has Subscription Conversion set to One per unit.
  • Contract Opportunity.
    • This solution will separate the Subscriptions into one record per unit just like the Asset Conversion field does.
  • Amend this Contract.
    • CPQ will combine all Subscriptions into one Quote Line on the Amendment Quote.
  • Contract this Amendment Opportunity.
    • Updated Quantities on the Amendment Quote will produce similar separated out Subscription records.
  • Check the Renewal Forecast/Quoted checkbox(es).
    • CPQ will combine all Subscriptions into one Quote Line on the Renewal Quote.

Custom Fields
There are two Custom Fields that are involved in this solution.

The first field is on the Product object. It’s a Picklist called Subscription Conversion (just like Asset Conversion) with the same values as Asset Conversion. The “One per unit” value will separate the Subscriptions into different records with 1 Quantity.

The second field is a Formula on the Subscription that points to the above field.

Apex Class
There is one Apex Class that provides all of the magic for this. It is set up as a batch class in case you have a giant Quantity on your Subscriptions.

public class SubscriptionHelper implements Database.Batchable<sObject> {
    public static boolean ranSplitSubs = false;
    public List<SBQQ__Subscription__c> subsToInsert;
    
    // function to split subscriptions by quantity if the subscription's product's subscription conversion field is set to One per unit.
    @InvocableMethod
    public static void createNewSplitSubscriptionsByQuantity(List<SBQQ__Subscription__c> subs) {
        if (!SubscriptionHelper.ranSplitSubs) {
            SubscriptionHelper.ranSplitSubs = true;
            
            // create subscriptions to insert list.
            List<SBQQ__Subscription__c> subsToInsert = new List<SBQQ__Subscription__c>();
            
            // create subscriptions to update list.
            Map<Id, SBQQ__Subscription__c> subsToUpdateMap = new Map<Id, SBQQ__Subscription__c>();
            
            // loop through subscriptions.
            for (SBQQ__Subscription__c s : subs) {
                // if this subscription's product has the subscription conversion field set to One per unit,
                if (s.Subscription_Conversion__c == 'One per unit' && s.SBQQ__Quantity__c > 1 || s.SBQQ__Quantity__c < 1) {
                    // add original subscription to subscriptions to update list.
                    subsToUpdateMap.put(s.id, s);
                    
                    // loop subscription . quantity times.
                    for (decimal i = 1; i < Math.abs(s.SBQQ__Quantity__c); i++) {
                        // clone subscription.
                        SBQQ__Subscription__c cs = s.clone();
                        
                        // set pertinent values on duplicate subscriptions.
                        cs.id = null;
                        cs.SBQQ__Quantity__c = s.SBQQ__Quantity__c > 1 ? 1 : -1;
                        cs.SBQQ__RenewalQuantity__c = s.SBQQ__Quantity__c > 1 ? 1 : -1;
                        if (cs.SBQQ__RevisedSubscription__c == null) {
                            cs.SBQQ__RevisedSubscription__c = s.id;
                        }
                        
                        // add cloned subscription to subscriptions to insert list.
                        subsToInsert.add(cs);
                    }
                }
            }
            
            // if we have subscriptions to update,
            if (subsToUpdateMap.size() > 0) {
                // query subscriptions to update.
                subsToUpdateMap = new Map<id, SBQQ__Subscription__c>([select Id, SBQQ__Quantity__c, SBQQ__RootId__c from SBQQ__Subscription__c where Id in :subsToUpdateMap.keySet()]);
                
                // loop through subscriptions.
                for (SBQQ__Subscription__c s : subsToUpdateMap.values()) {
                    // make updates.
                    s.SBQQ__Quantity__c = s.SBQQ__Quantity__c > 1 ? 1 : -1;
                    s.SBQQ__RenewalQuantity__c = s.SBQQ__Quantity__c > 1 ? 1 : -1;
                    s.SBQQ__RootId__c = s.id;
                }
                
                // update subscriptions to update.
                update subsToUpdateMap.values();
            }
            
            // if we have subscriptions to insert,
            if (subsToInsert.size() > 0) {
                // insert subscriptions to insert.
                Id batchId = Database.executeBatch(new SubscriptionHelper(subsToInsert), 100);
            }
        }
    }
    
    public SubscriptionHelper(List<SBQQ__Subscription__c> subs) { subsToInsert = subs; }
    public Iterable<sObject> start(Database.BatchableContext bc) { return subsToInsert; }
    public void execute(Database.BatchableContext bc, List<SBQQ__Subscription__c> records) { insert records; }
    public void finish(Database.BatchableContext bc) {}
}

Test Apex Class
Here’s the Test Apex Class for the above Apex Class.

@isTest
public class SubscriptionHelperTester {
    @isTest static void test() {
        Product2 p = new Product2(Name='Test', IsActive=True, Subscription_Conversion__c='One per unit');
        insert p;
        Account a = new Account(Name='Test');
        insert a;
        Contract c = new Contract(AccountId=a.id);
        insert c;
        
        Test.startTest();
        SBQQ__Subscription__c s = new SBQQ__Subscription__c(SBQQ__Product__c = p.id, SBQQ__Quantity__c=5);
        insert s;
        s = [select Id, SBQQ__Quantity__c, SBQQ__RootId__c, Subscription_Conversion__c, SBQQ__RevisedSubscription__c from SBQQ__Subscription__c where Id = :s.id];
        SubscriptionHelper.createNewSplitSubscriptionsByQuantity(new List<SBQQ__Subscription__c>{ s });
        Test.stopTest();
    }
}

Process Builder
This Process Builder executes on create only and calls the Apex Class above in a Scheduled Action.

Install Links
If you are here at the end, you deserve a great reward! Below are some install links for all of the above configuration! Install and modify as needed.
Production | Sandbox

D P

2 thoughts on “Subscription Conversion – One per unit

    1. Are you talking about the GSA’s CALM system that’s been under development (I’m not sure what CALM is) or a CLM system that interacts with Salesforce?

      If the latter, this solution, is by no means, a replacement for a CLM. However, I’ve had many experiences where CLM tools get in the way of a well functioning CPQ implementation. I always end up only using the red lining features of the CLM tool and turning off everything else.

Leave a Reply to D P Cancel reply

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

This site uses Akismet to reduce spam. Learn how your comment data is processed.