Custom Action Plugin
Use the Custom Action Plugin to configure additional actions you want to trigger via Zuora Rules Engine. The Custom Action Plugin interface class, zqu.CustomActionPlugin
, contains the following interface methods:
perform
isUpdateAction
isAddRemoveAction
isValidateAction
Perform Method
In the perform
method, define the logic this custom action will execute. See the sample code below for an example implementation of the perform
method.
The method takes the following input parameters.
Input |
Data Type |
Description |
---|---|---|
masterObject | zqu.DataObject | Contains the record that the rule is currently running against and its related data. This will generally be a Quote record. |
attributes | Map<String, Object> | A map of parameters that can be defined by an admin when configuring a custom action. |
logs | String[] | A list of strings that get printed to the Rules Engine log when the rules engine executes the action. |
Rule Types Methods
When initiating the Rules Engine programmatically, you have an option to specify which types of actions to run. For example, calling the Rules Engine with the Rule Type of VALIDATION will only evaluate rules with a Validation action.
By default, the Rules Engine runs all actions.
When defining a custom action, set the following methods to return true
to specify the types of this action.
Method |
Rule Types that Can Invoke this Action |
---|---|
isUpdateAction() |
ALL or PRICE |
isAddRemoveAction() |
ALL or PRODUCT |
isValidateAction() |
ALL or VALIDATION |
Constructors
Using the following constructors, you can instantiate different types of DataObjects to add new data during a custom action.
Contructor | Description |
---|---|
new ZChargeGroupDataObject new ZChargeGroupDataObject |
Creates a new ChargeGroup, retrieved through the zqu.zQuoteUtil.getChargeGroup global method. QuoteRatePlan fields can be retrieved from this record, even if it has not yet been saved to the database. Charge objects can be retrieved from an instance of this object using: getChildren('zqu__QuoteRatePlanCharge__c') |
new PlaceholderChargeGroupDataObject |
Represents a charge group to be added after rules execution completes. Charge and Plan details are not accessible from this instance, but because it does not call getChargeGroups, it runs more quickly and makes no SOQL queries. |
new CommittedDataObject |
Represents an SObject record inserted into the Database. Not used for Product Actions. |
Sample Code
As of version 10.11, this Sample Code can be applied to CPQ 9 (legacy UI) and CPQ X (new Quote Studio UI) based on the conditional logic for X or 9 seen in the code below. In case you already have CPQ 9 configured, all you need to do is add the CPQ X conditional logic, and if you are only going to be using CPQ X, you can exclude the CPQ 9 conditions.
Additionally, if the unmanaged Custom Action Plugin class was created before version 10.11, the metadata of the class needs to be updated to version 10.11 or higher.
The below sample code implements a custom action that updates charges, removes and adds rate plans, and validates the quote.
global with sharing class MyCustomActionPlugin implements zqu.CustomActionPlugin { global void perform(zqu.DataObject masterObject, Map<String, Object> attributes, String[] logs) { String quoteId = (String) masterObject.get('Id'); String attributeMsg = (String) attributes.get('Application'); List <zqu.DataObject> plans = masterObject.getChildren('zqu__QuoteRatePlan__c'); List <zqu.DataObject> charges = masterObject.getChildren('zqu__QuoteRatePlanCharge__c'); Boolean isQuoteStudioProcess = false; for (zqu.DataObject plan : plans) { if (plan instanceof zqu.QPlanDataObject) { isQuoteStudioProcess = true; break; } } // Log Attributes. logs.add('Quote Id : ' + quoteId); logs.add('Attribute Message : ' + attributeMsg); logs.add('Is Quote Studio Process : ' + isQuoteStudioProcess); logs.add('Number Of Plans : ' + plans.size()); logs.add('Number Of Charges : ' + charges.size()); // ------------ ADD PRODUCT ACTION ------------ // //Note - Ensure that the Product Rate Plan Id corresponds to the product you wish to process.// ID productRatePlanIdToAdd = [select id from zqu__ProductRatePlan__c limit 1].id; zqu.DataObject toBeAddedPlan = null; // CASE 1 // toBeAddedPlan = new zqu.PlaceholderChargeGroupDataObject(productRatePlanIdToAdd); toBeAddedPlan.flagAdded(); masterObject.addChild('zqu__QuoteRatePlan__c', toBeAddedPlan); // CASE 2 // toBeAddedPlan = null; if (isQuoteStudioProcess) { // CPQ X Rules Engine Flow. toBeAddedPlan = new zqu.QPlanDataObject(quoteId, productRatePlanIdToAdd); } else { // CPQ 9 Rules Engine Flow. toBeAddedPlan = new zqu.ZChargeGroupDataObject(masterObject, productRatePlanIdToAdd); } zqu__ProductRatePlan__c prp = new zqu__ProductRatePlan__c(Id = productRatePlanIdToAdd); zqu.DataObject dataProductRatePlan = new zqu.CommittedDataObject(prp); toBeAddedPlan.putParent('zqu__ProductRatePlan__c', dataProductRatePlan); toBeAddedPlan.flagAdded(); masterObject.addChild('zqu__QuoteRatePlan__c', toBeAddedPlan); // ------------ UPDATE PRODUCT ACTION ------------ // Decimal chargeListPrice = 10, tierListPrice = 10, quantity = 20, tierDiscount = 50; for (zqu.DataObject charge : charges) { // Update the list price at the charge level for 'Flat Fee Pricing' charge type. if (charge.get('zqu__Model__c') == 'Flat Fee Pricing') { charge.put('zqu__ListPrice__c', chargeListPrice); logs.add('Updating list Price of (' + charge.get('Name') + ') to ' + chargeListPrice + '.'); chargeListPrice += 10; } // Update the list price at the charge level for 'Per Unit Pricing' charge type. if (charge.get('zqu__Model__c') == 'Per Unit Pricing') { charge.put('zqu__Quantity__c', quantity); logs.add('Updating quantity of (' + charge.get('Name') + ') to ' + quantity + '.'); quantity += 10; } // Update the tier details (List Price, Discount & Effective Price). if (charge.get('zqu__ChargeType__c') == 'Usage' && (charge.get('zqu__Model__c') == 'Tiered Pricing' || charge.get('zqu__Model__c') == 'Volume Pricing')) { // CPQ 9 Rules Engine Flow. if (!isQuoteStudioProcess) { zqu.ZChargeDataObject chargeObject = (zqu.ZChargeDataObject) charge; List<zqu__QuoteCharge_Tier__c> tiers = chargeObject.getCharge().chargeTiersObjects; if (tiers != null && tiers.size() > 0) { for (zqu__QuoteCharge_Tier__c tier : tiers) { tier.zqu__Price__c = tierListPrice; tier.zqu__Discount__c = tierDiscount; tier.zqu__Effective_Price__c = (tierListPrice / 2); tierListPrice += 10; } logs.add('Updating the tiers of charge (' + charge.get('Name') + ').'); // Populate the changed tier information. chargeObject.rePopulateCustomChargeTiers(tiers); } } // CPQ X Rules Engine Flow. if (isQuoteStudioProcess) { zqu.QChargeDataObject chargeObject = (zqu.QChargeDataObject) charge; if (chargeObject != null) { zqu.QCharge qc = chargeObject.getQCharge(); if (qc != null) { List<zqu.QTier> tiers = qc.getTiers(); if (tiers != null && tiers.size() > 0) { for (zqu.QTier tier : tiers) { tier.put('zqu__Price__c', tierListPrice); tier.put('zqu__Discount__c', tierDiscount); tier.put('zqu__Effective_Price__c', (tierListPrice / 2)); tierListPrice += 10; } // Update the tiers in the charge object. qc.setTiers(tiers); } } } } } } // ------------ REMOVE PRODUCT ACTION ------------ // String productToRemove = 'Annually Billed Per Unit Product'; for (zqu.DataObject plan : plans) { String productName = (String) plan.get('zqu__QuoteProductName__c'); System.debug('Product Name : ' + productName); if (productName == productToRemove) { logs.add('Product Found : ' + productName); // Mark As Removed. plan.flagRemoved(); logs.add('Removed : ' + productName); } } // ------------ VALIDATION ERROR ------------ // if (plans.size() > 2) { throw new zqu.CustomValidationException('You cannot have more than two plans on a quote.'); } } global Boolean isUpdateAction() { return true; } global Boolean isAddRemoveAction() { return true; } global Boolean isValidateAction() { return true; } }
Below is a sample code with a test class for the custom action plugin.
@isTest global class MyCustomActionPluginTest { public static final String DEFAULT_BILLTO_CONTACT_FIRST_NAME = 'Test Bill-To First Name'; public static final String DEFAULT_BILLTO_CONTACT_LAST_NAME = 'Test Bill-To Last Name'; public static final String DEFAULT_SOLDTO_CONTACT_FIRST_NAME = 'Test Sold-To First Name'; public static final String DEFAULT_SOLDTO_CONTACT_LAST_NAME = 'Test Sold-To Last Name'; public static final String DEFAULT_OPPORTUNITY_NAME = 'Test Opportunity Name'; public static final String DEFAULT_ACCOUNT_NAME = 'Test Salesforce Account Name'; public static final String DEFAULT_CURRENCY = 'USD'; public static final Date DEFAULT_EFFECTIVE_START_DATE = System.today(); public static final Date DEFAULT_EFFECTIVE_END_DATE = System.today().addYears(1); public static final String NEW_SUBSCRIPTION = 'New Subscription'; public static final String MOCK_ZUORA_PROD_ID = '1118e487366202430136757c960716b6'; @TestSetup private static void setupTestData(){ //create a test account Account testaccount = new Account(Name = DEFAULT_ACCOUNT_NAME); insert testaccount; system.assertEquals([SELECT count() from Account],1); //create billto,soldTo contacts Contact billToContact = new Contact(FirstName = DEFAULT_BILLTO_CONTACT_FIRST_NAME, LastName = DEFAULT_BILLTO_CONTACT_LAST_NAME); billToContact.accountId = testaccount.Id; insert billToContact; Contact soldToContact = new Contact(FirstName = DEFAULT_SOLDTO_CONTACT_FIRST_NAME, LastName = DEFAULT_SOLDTO_CONTACT_LAST_NAME); soldToContact.accountId = testaccount.Id; insert soldToContact; system.assertEquals([SELECT count() from Contact],2); //create an opportunity Opportunity testopportunity = new Opportunity(Name = DEFAULT_OPPORTUNITY_NAME, AccountId = testaccount.Id); testopportunity.StageName = 'Closed Won'; testopportunity.CloseDate = System.today(); insert testopportunity; system.assertEquals([SELECT count() from Opportunity],1); //create a zuora quote zqu__Quote__c quote = new zqu__Quote__c(); quote.zqu__Currency__c = DEFAULT_CURRENCY; quote.Name = 'Positive Quote'; quote.zqu__Account__c = testaccount.Id; quote.zqu__AutoRenew__c = true; quote.zqu__Amendment_Name__c = 'Positive Amendment'; quote.zqu__Opportunity__c = testopportunity.Id; quote.zqu__BillToContact__c = billToContact.Id; quote.zqu__SoldToContact__c = soldToContact.Id; quote.zqu__InitialTerm__c = 12.0; quote.zqu__RenewalTerm__c = 6.0; quote.zqu__PaymentMethod__c = 'Credit Card'; quote.zqu__ValidUntil__c = DEFAULT_EFFECTIVE_START_DATE; quote.zqu__StartDate__c = DEFAULT_EFFECTIVE_START_DATE; quote.zqu__SubscriptionTermStartDate__c = DEFAULT_EFFECTIVE_START_DATE; quote.zqu__SubscriptionTermEndDate__c = DEFAULT_EFFECTIVE_END_DATE; quote.zqu__SubscriptionType__c = NEW_SUBSCRIPTION; quote.zqu__BillingMethod__c = 'Both'; quote.zqu__Subscription_Term_Type__c = 'Termed'; insert quote; system.assertEquals([SELECT count() from zqu__Quote__c],1); //creating ZProducts zqu__ZProduct__c zuoraproduct = new zqu__ZProduct__c(); zuoraproduct.Name = 'CC Testing'; zuoraproduct.zqu__EffectiveStartDate__c = DEFAULT_EFFECTIVE_START_DATE; zuoraproduct.zqu__EffectiveEndDate__c = DEFAULT_EFFECTIVE_END_DATE; zuoraproduct.zqu__SKU__c = 'testingsku0001'; zuoraproduct.zqu__ZuoraId__c = 'zuoraid0000000001'; zuoraproduct.zqu__Deleted__c = false; insert zuoraproduct; system.assertEquals([SELECT count() from zqu__ZProduct__c],1); //creating salesforce product Product2 testsfproduct = new Product2(); testsfproduct.Name = 'CC Testing'; testsfproduct.zqu__EffectiveStartDate__c = DEFAULT_EFFECTIVE_START_DATE; testsfproduct.zqu__EffectiveEndDate__c = DEFAULT_EFFECTIVE_END_DATE; testsfproduct.zqu__SKU__c = 'testingsku0001'; testsfproduct.zqu__ZuoraId__c = 'zuoraid0000000001'; testsfproduct.zqu__Deleted__c = false; insert testsfproduct; system.assertEquals([SELECT count() from Product2],1); //creating Product rate plan zqu__ProductRatePlan__c testproductrateplan = new zqu__ProductRatePlan__c(); testproductrateplan.Name = 'Test Product Rate Plan'; testproductrateplan.zqu__EffectiveStartDate__c = DEFAULT_EFFECTIVE_START_DATE; testproductrateplan.zqu__EffectiveEndDate__c = DEFAULT_EFFECTIVE_END_DATE; testproductrateplan.zqu__Product__c = testsfproduct.Id; testproductrateplan.zqu__ZProduct__c = zuoraproduct.Id; testproductrateplan.zqu__ZuoraId__c = MOCK_ZUORA_PROD_ID; testproductrateplan.zqu__Deleted__c = false; testproductrateplan.zqu__ActiveCurrencies__c = DEFAULT_CURRENCY; insert testproductrateplan; system.assertEquals([SELECT count() from zqu__ProductRatePlan__c],1); //creating Product rate plan charge with Model as 'Flat Fee Pricing' and Type as 'Recurring' zqu__ProductRatePlanCharge__c productrateplancharge = new zqu__ProductRatePlanCharge__c(); productrateplancharge.Name = 'Test Product Rate Plan Charge'; productrateplancharge.zqu__Model__c = 'Flat Fee Pricing'; productrateplancharge.zqu__Type__c = 'Usage'; productrateplancharge.zqu__UOM__c = 'UOM tesing'; productrateplancharge.zqu__DefaultQuantity__c = 1; productrateplancharge.zqu__MinQuantity__c = 0; productrateplancharge.zqu__MaxQuantity__c = 500; productrateplancharge.zqu__RecurringPeriod__c = 'Month'; productrateplancharge.zqu__ZuoraId__c = MOCK_ZUORA_PROD_ID; productrateplancharge.zqu__ProductRatePlan__c = testproductrateplan.Id; productrateplancharge.zqu__Deleted__c = false; if (productrateplancharge.zqu__Model__c == 'Discount-Fixed Amount' || productrateplancharge.zqu__Model__c == 'Discount-Percentage') { productrateplancharge.zqu__Discount_Apply_Type__c = 3; productrateplancharge.zqu__Upto_How_Many_Periods__c = 5; productrateplancharge.zqu__Discount_Level__c = 'RatePlan'; } // Set List Price for non-tiered charge productrateplancharge.zqu__ListPrice__c = 1000; //creating Product rate plan charge with Model as 'Flat Fee Pricing' and Type as 'Recurring' zqu__ProductRatePlanCharge__c productrateplanchargeflatfee = new zqu__ProductRatePlanCharge__c(); productrateplanchargeflatfee.Name = 'Test Flat Fee Product Rate Plan Charge'; productrateplanchargeflatfee.zqu__Model__c = 'Flat Fee Pricing'; productrateplanchargeflatfee.zqu__Type__c = 'Usage'; productrateplanchargeflatfee.zqu__UOM__c = 'UOM tesing'; productrateplanchargeflatfee.zqu__DefaultQuantity__c = 1; productrateplanchargeflatfee.zqu__MinQuantity__c = 0; productrateplanchargeflatfee.zqu__MaxQuantity__c = 500; productrateplanchargeflatfee.zqu__RecurringPeriod__c = 'Month'; productrateplanchargeflatfee.zqu__ZuoraId__c = MOCK_ZUORA_PROD_ID; productrateplanchargeflatfee.zqu__ProductRatePlan__c = testproductrateplan.Id; productrateplanchargeflatfee.zqu__Deleted__c = false; productrateplanchargeflatfee.zqu__ListPrice__c = 1000; //creating Product rate plan charge with Model as 'Tiered Pricing' and Type as 'Recurring' zqu__ProductRatePlanCharge__c productrateplanchargetierpricing = new zqu__ProductRatePlanCharge__c(); productrateplanchargetierpricing.Name = 'Test Tiered Pricing Product Rate Plan Charge'; productrateplanchargetierpricing.zqu__Model__c = 'Tiered Pricing'; productrateplanchargetierpricing.zqu__Type__c = 'Usage'; productrateplanchargetierpricing.zqu__UOM__c = 'UOM tesing'; productrateplanchargetierpricing.zqu__DefaultQuantity__c = 1; productrateplanchargetierpricing.zqu__MinQuantity__c = 0; productrateplanchargetierpricing.zqu__MaxQuantity__c = 500; productrateplanchargetierpricing.zqu__RecurringPeriod__c = 'Month'; productrateplanchargetierpricing.zqu__ZuoraId__c = '1118e487366202430136757c960715b5'; productrateplanchargetierpricing.zqu__ProductRatePlan__c = testproductrateplan.Id; productrateplanchargetierpricing.zqu__Deleted__c = false; productrateplanchargetierpricing.zqu__ListPrice__c = 1000; List<zqu__ProductRatePlanCharge__c> productrateplanchargeList = new List<zqu__ProductRatePlanCharge__c>{productrateplanchargeflatfee,productrateplanchargetierpricing}; insert productrateplanchargeList; system.assertEquals([SELECT count() from zqu__ProductRatePlanCharge__c],2); //create quote amendment zqu__QuoteAmendment__c testQuoteAmendment = new zqu__QuoteAmendment__c(); testQuoteAmendment.zqu__Quote__c = quote.Id; insert testQuoteAmendment; system.assertEquals([SELECT count() from zqu__QuoteAmendment__c],1); //create quote rate plan zqu__QuoteRatePlan__c testQuoteRatePlan = new zqu__QuoteRatePlan__c(); testQuoteRatePlan.zqu__Quote__c = quote.Id; testQuoteRatePlan.zqu__ProductRatePlan__c = testproductrateplan.Id; testQuoteRatePlan.zqu__QuoteAmendment__c = testQuoteAmendment.Id; testQuoteRatePlan.zqu__QuoteProductName__c = 'Orange Box'; insert testQuoteRatePlan; system.assertEquals([SELECT count() from zqu__QuoteRatePlan__c],1); //create quote rate plan charge List<zqu__QuoteRatePlanCharge__c> testQuoteRatePlanChargeList = new List<zqu__QuoteRatePlanCharge__c>(); List<zqu__QuoteCharge_Tier__c> testquotechargetierList = new List<zqu__QuoteCharge_Tier__c>(); List<zqu__ProductRatePlanChargeTier__c> testproductrateplanchargetierList = new List<zqu__ProductRatePlanChargeTier__c>(); for(Integer productrateplanchargeIterator = 0;productrateplanchargeIterator < productrateplanchargeList.size();productrateplanchargeIterator++) { zqu__QuoteRatePlanCharge__c testQuoteRatePlanCharge = new zqu__QuoteRatePlanCharge__c(); testQuoteRatePlanCharge.zqu__QuoteRatePlan__c = testQuoteRatePlan.Id; testQuoteRatePlanCharge.zqu__ProductRatePlanCharge__c = productrateplanchargeList[productrateplanchargeIterator].Id; testQuoteRatePlanCharge.zqu__Model__c = productrateplanchargeList[productrateplanchargeIterator].zqu__Model__c; testQuoteRatePlanCharge.zqu__ChargeType__c = productrateplanchargeList[productrateplanchargeIterator].zqu__Type__c; testQuoteRatePlanChargeList.add(testQuoteRatePlanCharge); //create product rate plan charge tier zqu__ProductRatePlanChargeTier__c prodchargetier = new zqu__ProductRatePlanChargeTier__c( zqu__Active__c = false, zqu__Currency2__c = DEFAULT_CURRENCY, zqu__Currency__c = DEFAULT_CURRENCY, zqu__Deleted__c = false, zqu__EndingUnit__c = 0.0, zqu__IsOveragePrice__c = false, Name = '1', zqu__PriceFormat2__c ='Flat Fee', zqu__PriceFormat__c ='Flat Fee', zqu__Price__c = 1000, zqu__ProductRatePlanCharge__c = productrateplanchargeList[productrateplanchargeIterator].Id, zqu__StartingUnit__c = 0.0, zqu__Tier__c = 1 ); testproductrateplanchargetierList.add(prodchargetier); } insert testQuoteRatePlanChargeList; insert testproductrateplanchargetierList; system.assertEquals([SELECT count() from zqu__QuoteRatePlanCharge__c],2); system.assertEquals([SELECT count() from zqu__ProductRatePlanChargeTier__c],2); //create quote charge tier for(Integer quoterateplanchargeIterator = 0;quoterateplanchargeIterator < testQuoteRatePlanChargeList.size();quoterateplanchargeIterator++){ //create quote charge tier if(testQuoteRatePlanChargeList[quoterateplanchargeIterator].zqu__Model__c == 'Tiered Pricing') { zqu__QuoteCharge_Tier__c tier = new zqu__QuoteCharge_Tier__c( Name = String.valueOf('Tier ' + quoterateplanchargeIterator + 1), zqu__Tier__c = quoterateplanchargeIterator + 1, zqu__Price__c = quoterateplanchargeIterator + 1, zqu__Discount__c = 0, zqu__Effective_Price__c = 1000, zqu__StartingUnit__c = quoterateplanchargeIterator, zqu__EndingUnit__c = quoterateplanchargeIterator + 1, zqu__IsOveragePrice__c = false, zqu__QuoteRatePlanCharge__c = testQuoteRatePlanChargeList[quoterateplanchargeIterator].Id ); testquotechargetierList.add(tier); } } insert testquotechargetierList; system.assertEquals([SELECT count() from zqu__QuoteCharge_Tier__c],1); } @isTest private static void test_performCPQX() { zqu__Quote__c testquote = [SELECT Id from zqu__Quote__c LIMIT 1]; system.assertNotEquals(testquote,null); zqu__QuoteRatePlan__c testQuoteRatePlan = [SELECT Id, zqu__QuoteProductName__c, zqu__ProductRatePlan__c from zqu__QuoteRatePlan__c WHERE zqu__Quote__c =:testquote.Id]; system.assertNotEquals(testQuoteRatePlan,null); //Prepare the masterobject with parent object as zqu__Quote__c zqu.DataObject masterObject = new zqu.CommittedDataObject(testquote); system.assertNotEquals(masterObject,null); //Add the Quote Rate Plan Object as first child of the parent zqu__Quote__c zqu.DataObject rateplanObj = new zqu.CommittedDataObject(testQuoteRatePlan); system.assertNotEquals(rateplanObj,null); masterObject.addChild('zqu__QuoteRatePlan__c',rateplanObj); List<zqu.QPlan> qPlans= zqu.QPlanBuilder.makeFromCatalog(testquote.Id,new List<Id>{testQuoteRatePlan.zqu__ProductRatePlan__c}); for (zqu.QPlan qPlan : qPlans) { zqu.QPlanDataObject objList = new zqu.QPlanDataObject(qPlan); masterObject.addChild('zqu__QuoteRatePlan__c',objList); for(zqu.QCharge qCharge : qPlan.getCharges()) { zqu.QChargeDataObject obj = new zqu.QChargeDataObject(qCharge); masterObject.addChild('zqu__QuoteRatePlanCharge__c',obj); } } Map<String, Object> attributes = new Map<String, Object>(); List<String> logs = new List<String>(); Test.startTest(); MyCustomActionPlugin customPlugin = new MyCustomActionPlugin(); customPlugin.perform(masterObject, attributes, logs); customPlugin.isUpdateAction(); customPlugin.isAddRemoveAction(); customPlugin.isValidateAction(); Test.stopTest(); } @isTest private static void test_performCPQ9() { zqu__Quote__c testquote = [SELECT Id from zqu__Quote__c LIMIT 1]; system.assertNotEquals(testquote,null); zqu__QuoteRatePlan__c testQuoteRatePlan = [SELECT Id, zqu__QuoteProductName__c, zqu__ProductRatePlan__c from zqu__QuoteRatePlan__c WHERE zqu__Quote__c =:testquote.Id]; system.assertNotEquals(testQuoteRatePlan,null); //Prepare the masterobject with parent object as zqu__Quote__c zqu.DataObject masterObject = new zqu.CommittedDataObject(testquote); system.assertNotEquals(masterObject,null); //Add the Quote Rate Plan Object as first child of the parent zqu__Quote__c zqu.DataObject rateplanObj = new zqu.CommittedDataObject(testQuoteRatePlan); system.assertNotEquals(rateplanObj,null); masterObject.addChild('zqu__QuoteRatePlan__c',rateplanObj); //Get the Quote Rate Plan Charge Groups and add Quote Rate Plan Charge & Tier details as children List<zqu.zChargeGroup> chargeGroups = zqu.zQuoteUtil.getChargeGroups(testquote.Id, new List<Id>{testQuoteRatePlan.zqu__ProductRatePlan__c}); system.assertNotEquals(chargeGroups,null); for (zqu.zChargeGroup cg : chargeGroups) { zqu.DataObject dataCg = new zqu.ZChargeGroupDataObject(cg); for(zqu.DataObject chargedata : dataCg.getChildren('zqu__QuoteRatePlanCharge__c')) { masterObject.addChild('zqu__QuoteRatePlanCharge__c',chargedata); } } Map<String, Object> attributes = new Map<String, Object>(); List<String> logs = new List<String>(); Test.startTest(); MyCustomActionPlugin customPlugin = new MyCustomActionPlugin(); customPlugin.perform(masterObject, attributes, logs); customPlugin.isUpdateAction(); customPlugin.isAddRemoveAction(); customPlugin.isValidateAction(); Test.stopTest(); } }
The sample code below implements a custom action plugin for Zuora rules, providing developers with access to ramp intervals set up on quotes.
The masterObject parameter passed to the perform method makes the ramp intervals accessible using a object name RampIntervals. This allows developers to interact with ramp intervals during the execution of the custom action. The following fields are set on the RampIntervals object:
- Index: An index value staring with 0 (e.g. 0, 1, 2, 3, ...) representing the interval's position in the sequence.
- IntervalStartDate: The start date of the interval.
- IntervalEndDate: The end date of the interval.
global class CustomActionWithRampDetails implements zqu.CustomActionPlugin { global void perform(zqu.DataObject masterObject, Map<String, Object> attributes, String[] logs) { // Get all ramp intervals. // Each interval contains below fields // Index: Index of interval start with 0(Example 0,1,2...) // IntervalStartDate: Start Date Of Interval // IntervalEndDate: End Date Of Interval List<zqu.DataObject> ramps = masterObject.getChildren('RampIntervals'); integer activeIntervalIndex=0; for (zqu.DataObject ramp : ramps) { if(ramp.get('IsActive')!=null && (Boolean)ramp.get('IsActive')){ activeIntervalIndex = integer.valueOf(ramp.get('Index')); } logs.add('Interval Index: '+ramp.get('Index')); logs.add('Start Date: '+ramp.get('IntervalStartDate')); logs.add('End Date: '+ramp.get('IntervalEndDate')); } logs.add('Active Interval Index:'+activeIntervalIndex); } public Boolean isAddRemoveAction(){return true;} public Boolean isUpdateAction(){return true;} public Boolean isValidateAction(){return true;} }
Ramp interval details are available only when custom action plugin is called with CPQ X (Quote Studio).
Updating charge using a specific update date
Use the Custom Action Plugin to update the charge using a specific update date.
If you want to update the previous segment charge, you need to set the settings in Zuora Config > Quote Studio Settings > Admin Config for Define Behavior for making Updates before Future Dated Updates to Automatically set the Specific Update Date whenever an update occurs before the last segment of the original rate plan (Future Dated Update).
Below is the sample code to update product B’s charge in the same and future segments if product A’s charge is updated in the same interval for new, amendment, and renewal subscriptions.
global with sharing class GenerateUpdatedLinesCAP implements zqu.CustomActionPlugin { global void perform(zqu.DataObject masterObject, Map<String, Object> attributes, String[] logs) { String quoteId = (String) masterObject.get('Id'); String msg = (String) attributes.get('msg'); List <zqu.DataObject> plans = masterObject.getChildren('zqu__QuoteRatePlan__c'); List <zqu.DataObject> charges = masterObject.getChildren('zqu__QuoteRatePlanCharge__c'); System.debug('Plans size in CAP'+plans.size()); System.debug('charges size in CAP'+charges.size()); Boolean isQuoteStudioProcess = false; for (zqu.DataObject plan : plans) { if (plan instanceof zqu.QPlanDataObject) { isQuoteStudioProcess = true; break; } } // Log Attributes. logs.add('Quote Id : ' + quoteId); logs.add('Attribute Message : ' + msg); logs.add('Is Quote Studio Process : ' + isQuoteStudioProcess); logs.add('Number Of Plans : ' + plans.size()); logs.add('Number Of Charges : ' + charges.size()); if( isQuoteStudioProcess ) { Decimal chargeListPrice = 10, tierListPrice = 10, quantity = 20, tierDiscount = 50; try { if( masterObject.get('zqu__SubscriptionType__c') != 'Cancel Subscription' ) { Date subscriptionTermEndDate =(Date) masterObject.get('zqu__SubscriptionTermEndDate__c'); Map<string,ChargeWrapper> chargeDateMap = getChargeWrapperMap(charges,subscriptionTermEndDate); List<zqu.DataObject> ramps = masterObject.getChildren('RampIntervals'); Map<Integer,List<zqu.DataObject>> rampIndexChargesMap = new Map<Integer,List<zqu.DataObject>>(); for (zqu.DataObject ramp : ramps) { logs.add('Interval Index: '+ramp.get('Index')); logs.add('Start Date: '+ramp.get('IntervalStartDate')); logs.add('End Date: '+ramp.get('IntervalEndDate')); Date rampStartDate = Date.valueOf(ramp.get('IntervalStartDate')); Date rampEndDate = Date.valueOf(ramp.get('IntervalEndDate')); for(zqu.DataObject chargeDateObject : charges ) { zqu.QCharge charge = ((zqu.QChargeDataObject)chargeDateObject).getQCharge(); logs.add('ChargeOverrideReferenceId__c'+charge.get('zqu__PreviewChargeId__c')); logs.add('RatePlanOverrideReferenceId__c'+charge.getParentPlan().get('zqu__RatePlanOverrideReferenceId__c')); Date chargeEffectiveStartDate = System.today(); Date chargeEffectiveEndDate = System.today(); if (charge.get('zqu__EffectiveStartDate__c')!=null && charge.get('zqu__EffectiveEndDate__c')!=null) { logs.add('Date With zqu.QCharge ,Name==>'+charge.get('Name')+',Start Date==>'+charge.get('zqu__EffectiveStartDate__c')+', End Date=>'+charge.get('zqu__EffectiveEndDate__c')); chargeEffectiveStartDate = Date.valueOf(charge.get('zqu__EffectiveStartDate__c') ); chargeEffectiveEndDate = Date.valueOf(charge.get('zqu__EffectiveEndDate__c') ); } else { String chargeKey = getChargeId(charge); ChargeWrapper chargeDate = chargeDateMap.get(chargeKey); chargeEffectiveStartDate = chargeDate.EffectiveStartDate; chargeEffectiveEndDate = chargeDate.EffectiveEndDate; logs.add('Date With Calculation Id==>'+chargeKey+',Name==>'+charge.get('Name')+',Start Date==>'+chargeDate.EffectiveStartDate+', End Date=>'+chargeDate.EffectiveEndDate); } logs.add('ramp index'+Integer.valueOf(ramp.get('Index'))); logs.add('rampStartDate'+rampStartDate); logs.add('rampEndDate'+rampEndDate); logs.add('chargeEffectiveStartDate'+chargeEffectiveStartDate); logs.add('chargeEffectiveEndDate'+chargeEffectiveEndDate); //Specific end date and One Time String chargeType = (String)charge.get('zqu__ChargeType__c'); if( (chargeEffectiveStartDate<=rampStartDate && ( chargeEffectiveEndDate>=rampEndDate || masterObject.get('zqu__SubscriptionType__c')=='Renew Subscription' ) ) || ( chargeType == 'One-Time' && chargeEffectiveStartDate >=rampStartDate && chargeEffectiveStartDate <=rampEndDate ) ) { Integer rampIndex = Integer.valueOf(ramp.get('Index')); if( !rampIndexChargesMap.containsKey( rampIndex ) ) rampIndexChargesMap.put( rampIndex, new List<zqu.DataObject>() ); rampIndexChargesMap.get( rampIndex ).add(chargeDateObject); } } } Integer price = 0; for( Integer rampIndex: rampIndexChargesMap.keySet() ) { logs.add('rampIndex'+rampIndex+'Number Of Charges==>'+rampIndexChargesMap.get(rampIndex).size()); Integer counter = 0; Date effectiveStartDate = System.today(); try { for (zqu.DataObject ramp : ramps) { Integer rampIndex1 = (Integer)ramp.get('Index'); if(rampIndex1 == rampIndex) { effectiveStartDate = (Date)ramp.get('IntervalStartDate'); } } price = price+5; Map<String, List<ChargeWrapper>> mapOfCharge = populateEffectiveStartDate( rampIndexChargesMap.get(rampIndex) ); zqu.DataObject sourceCharge; zqu.DataObject chargeToUpdate; String sourceChargeAmendmentType; String targetChargeAmendmentType; Date targetChargeCED = System.today(); Date sourceChargeCED = System.today(); for(String previewChargeId : mapOfCharge.keySet()) { List<ChargeWrapper> chageWrapperList = mapOfCharge.get( previewChargeId ); Integer chargeIndex = chageWrapperList.size()-1; logs.add('chageWrapperList[chargeIndex].chargeQAType'+chageWrapperList[chargeIndex].chargeQAType); if( chageWrapperList[0].chargeName == 'Product A Charge' && ( chageWrapperList[chargeIndex].chargeQAType == 'UpdateProduct' || chageWrapperList[chargeIndex].chargeQAType == 'NewProduct' ) ) { sourceCharge = chageWrapperList[chargeIndex].chargeRec; sourceChargeAmendmentType = chageWrapperList[chargeIndex].chargeQAType; zqu.QChargeDataObject chargeObject = (zqu.QChargeDataObject) sourceCharge ; zqu.QCharge qc = chargeObject.getQCharge(); zqu.DataObject planObj = sourceCharge.getParent('zqu__QuoteRatePlan__c'); sourceChargeCED = planObj.getParent('zqu__QuoteAmendment__c')!=null ? (Date) planObj.getParent('zqu__QuoteAmendment__c').get('zqu__ContractEffectiveDate__c') : (Date) qc.getParentPlan().getAmendment().get('zqu__ContractEffectiveDate__c'); } if( chageWrapperList[0].chargeName == 'Product B Charge' ) { chargeToUpdate = chageWrapperList[chargeIndex].chargeRec; targetChargeAmendmentType = chageWrapperList[chargeIndex].chargeQAType; zqu.QChargeDataObject chargeObject = (zqu.QChargeDataObject) chargeToUpdate ; zqu.QCharge qc = chargeObject.getQCharge(); zqu.DataObject planObj = chargeToUpdate.getParent('zqu__QuoteRatePlan__c'); targetChargeCED = planObj.getParent('zqu__QuoteAmendment__c')!=null ? (Date) planObj.getParent('zqu__QuoteAmendment__c').get('zqu__ContractEffectiveDate__c') : (Date) qc.getParentPlan().getAmendment().get('zqu__ContractEffectiveDate__c'); } } logs.add('sourceChargeAmendmentType @@@@'+sourceChargeAmendmentType); logs.add('targetChargeAmendmentType@@@@'+targetChargeAmendmentType); if( sourceCharge == null || chargeToUpdate == null ) continue; zqu.DataObject plan = chargeToUpdate.getParent('zqu__QuoteRatePlan__c'); logs.add('ramp index'+rampIndex); logs.add('sourceChargeAmendmentType'+sourceChargeAmendmentType +'-----------'+'targetChargeAmendmentType '+targetChargeAmendmentType ); logs.add('target Charge CED'+targetChargeCED ); logs.add('source charge CED'+sourceChargeCED); logs.add('condition'+( targetChargeCED < sourceChargeCED) ); logs.add('ramp start date'+effectiveStartDate); if( ( sourceChargeAmendmentType == 'UpdateProduct' || sourceChargeCED != targetChargeCED ) || (targetChargeAmendmentType == 'Original') ) //&& targetChargeCED < sourceChargeCED { logs.add('inside specific update'); if(sourceChargeCED < targetChargeCED ) { logs.add('spefific update -> targetChargeCED-->' + targetChargeCED); plan.put('zqu__SpecificUpdateDate__c',targetChargeCED); plan.put('SpecificUpdateRemoveDate',targetChargeCED); }else { logs.add('spefific update -> sourceChargeCED-->' + sourceChargeCED); plan.put('zqu__SpecificUpdateDate__c',sourceChargeCED); plan.put('SpecificUpdateRemoveDate',sourceChargeCED); } } Decimal effectivePrice = ((Decimal)chargeToUpdate.get('zqu__EffectivePrice__c'))+5; logs.add('effectivePrice'+effectivePrice); chargeToUpdate.put('zqu__EffectivePrice__c',effectivePrice); } catch(Exception e) { logs.add('Excection'+e.getMessage()); } } } } catch(Exception e) { logs.add('exception'+e.getStackTraceString()); } } } global Boolean isUpdateAction() { return true; } global Boolean isAddRemoveAction() { return true; } global Boolean isValidateAction() { return true; } public Map<string,ChargeWrapper> getChargeWrapperMap(List <zqu.DataObject> charges,Date subscriptionTermEndDate) { Map<String, List<ChargeWrapper>> mapOfCharge = populateEffectiveStartDate(charges); Map<String, ChargeWrapper> result = new Map<String, ChargeWrapper>(); for (String key : mapOfCharge.keySet()) { List<ChargeWrapper> chargeWrappers = mapOfCharge.get(key); System.debug('charge wrappers'+chargeWrappers); // Set EffectiveEndDate as the next element's EffectiveStartDate for (Integer index = 0; index < chargeWrappers.size(); index++) { if( chargeWrappers[index].Type=='One-Time') { chargeWrappers[index].EffectiveEndDate = chargeWrappers[index].EffectiveStartDate.addDays(1); } else { System.debug('index'+index+'effective start date'+chargeWrappers[index].EffectiveStartDate); System.debug('effective end date'+chargeWrappers[index].EffectiveEndDate); if (index < chargeWrappers.size() - 1) { chargeWrappers[index].EffectiveEndDate = chargeWrappers[index + 1].EffectiveStartDate; } else { chargeWrappers[index].EffectiveEndDate = subscriptionTermEndDate; // Last element has null end date } } result.put(chargeWrappers[index].Id, chargeWrappers[index]); } } return result; } public Map<string,List<ChargeWrapper>> populateEffectiveStartDate( List <zqu.DataObject> charges) { Map<string,List<ChargeWrapper>> mapOfCharge = new Map<string,List<ChargeWrapper>>(); for(zqu.DataObject chargeDateObject : charges) { zqu.QCharge charge = ((zqu.QChargeDataObject)chargeDateObject).getQCharge(); zqu.QAmendment amendment = charge.getParentPlan().getAmendment(); ChargeWrapper chargeWrapper=new ChargeWrapper(); zqu.DataObject planObj= (zqu.DataObject) chargeDateObject.getParent('zqu__QuoteRatePlan__c'); String chargeQAType = planObj.getParent('zqu__QuoteAmendment__c')!=null ? (String) planObj.getParent('zqu__QuoteAmendment__c').get('zqu__Type__c') : (String) charge.getParentPlan().getAmendment().get('zqu__Type__c'); chargeWrapper.Id = getChargeId(charge); chargeWrapper.chargeRec = chargeDateObject; chargeWrapper.chargeName = (String)chargeDateObject.get('Name'); chargeWrapper.chargeQAType = chargeQAType; chargeWrapper.Type = (String)charge.get('zqu__ChargeType__c'); chargeWrapper.EffectiveStartDate = (Date)amendment.get('zqu__ContractEffectiveDate__c'); string key = chargeWrapper.Id; if(charge.get('zqu__PreviewChargeId__c')!=null){ key = (String)charge.get('zqu__PreviewChargeId__c'); } if (!mapOfCharge.containsKey(key)) { mapOfCharge.put(key, new List<ChargeWrapper>()); } mapOfCharge.get(key).add(chargeWrapper); } // Sort each list based on EffectiveStartDate for (String key : mapOfCharge.keySet()) { mapOfCharge.get(key).sort(); } return mapOfCharge; } public string getChargeId(zqu.QCharge charge){ zqu.QPlan plan = charge.getParentPlan(); return plan.getUniquePlanIdentifier(); } public class ChargeWrapper implements Comparable{ public string Id; public Date EffectiveStartDate; public Date EffectiveEndDate; public zqu.DataObject chargeRec; public String chargeQAType; public String chargeName; public string Type; public Integer compareTo(Object wrapper ) { ChargeWrapper other = ((ChargeWrapper)wrapper); return -this.EffectiveStartDate.daysBetween(other.EffectiveStartDate); } } }
Adding/updating/removing product on a specific date
Use the Custom Action Plugin to add/update/remove a rate plan on a specific date.
Adding product
If the Contract Effective Date is present in the Quote Amendment object, the rate plan will be added on that date. If it is not present, the product will be added to the Quote Start Date.
Updating product
We have added a flag, SpecificUpdateRemoveDate, to the QPlan object to pass the update’s effective date.
Removing product:
We have added a flag, SpecificUpdateRemoveDate, to the QPlan object to pass the effective date of removal.
Below is a sample code for adding, updating, and removing products.
global class CustomActionPluginExample implements zqu.CustomActionPlugin { global void perform(zqu.DataObject masterObject, Map < String, Object > attributes, String[] logs) { List < zqu.DataObject > ramps = masterObject.getChildren('RampIntervals'); for (zqu.DataObject ramp : ramps) { //Add product in each interval //addProduct(masterObject,Date.valueOf(ramp.get('IntervalStartDate'))); Date intervalStartDate = Date.valueOf(ramp.get('IntervalStartDate')); if (ramp.get('Index') == 1) { updateProduct(masterObject, intervalStartDate, logs, 2); addProduct(masterObject, intervalStartDate, logs); } else if (ramp.get('Index') == 2) { removeProduct(masterObject, intervalStartDate, logs); } else if (ramp.get('Index') == 3) { //updateProduct(masterObject,intervalStartDate,logs,4); } } } global Boolean isUpdateAction() { return true; } global Boolean isAddRemoveAction() { return true; } global Boolean isValidateAction() { return true; } private void updateProduct(zqu.DataObject masterObject, Date startDate, String[] logs, integer multip) { String productToUpdate = 'Airtel'; List < zqu.DataObject > plans = masterObject.getChildren('zqu__QuoteRatePlan__c'); for (zqu.DataObject plan : plans) { String productName = (String) plan.get('Name'); logs.add('Product Name Update==>' + productName); if (productName == productToUpdate) { List < zqu.DataObject > charges = plan.getChildren('zqu__QuoteRatePlanCharge__c'); Boolean isChargeUpdated = false; for (zqu.DataObject charge : charges) { if (charge.get('zqu__ChargeType__c') == 'Recurring') { charge.put('zqu__Quantity__c', (double)charge.get('zqu__Quantity__c') * multip); isChargeUpdated = true; } } //Update Date if (isChargeUpdated) { plan.put('SpecificUpdateRemoveDate', startDate); } } } } private void removeProduct(zqu.DataObject masterObject, Date startDate, String[] logs) { String productToRemove = 'Rec Vol Test'; List < zqu.DataObject > plans = masterObject.getChildren('zqu__QuoteRatePlan__c'); for (zqu.DataObject plan : plans) { String productName = (String) plan.get('zqu__QuoteProductName__c'); logs.add('Product Name Remove==>' + productName); if (productName == productToRemove) { plan.flagRemoved(); plan.put('SpecificUpdateRemoveDate', startDate); } } } private void addProduct(zqu.DataObject masterObject, Date startDate, String[] logs){ String quoteId = (String)masterObject.get('Id'); zqu.DataObject toBeAddedPlan = null; Id productRatePlanIdToAdd = [select id, name from zqu__productrateplan__c where name = 'JIO' and zqu__Product__r.Name = 'Broadband'][0].Id; toBeAddedPlan = new zqu.QPlanDataObject(quoteId, productRatePlanIdToAdd); zqu__ProductRatePlan__c prp = new zqu__ProductRatePlan__c(Id = productRatePlanIdToAdd); zqu.DataObject dataProductRatePlan = new zqu.CommittedDataObject(prp); toBeAddedPlan.putParent('zqu__ProductRatePlan__c', dataProductRatePlan); zqu.QPlanDataObject qpdo = (zqu.QPlanDataObject) toBeAddedPlan; zqu.QPlan planRec = (zqu.QPlan)qpdo.getQPlan(); zqu.QAmendment qa = planRec.getAmendment(); qa.put('zqu__ContractEffectiveDate__c', startDate); qa.put('zqu__ServiceActivationDate__c', startDate); qa.put('zqu__CustomerAcceptanceDate__c', startDate); toBeAddedPlan.flagAdded(); masterObject.addChild('zqu__QuoteRatePlan__c', toBeAddedPlan); logs.add('Product Added'); } }