-
-
Save paustint/40b602503b6cd6ae879af7b85d910da8 to your computer and use it in GitHub Desktop.
/** | |
* | |
* @description Data models for interacting with Steelbrick CPQ | |
* // https://community.steelbrick.com/t5/Developer-Guidebook/Public-API-Technical-Documentation-amp-Code-1/ta-p/5690 | |
* | |
*/ | |
public without sharing class CPQ_ApiDataModels { | |
/** INPUT PAYLOADS */ | |
public class RenewalContext { | |
public Id masterContractId; | |
public Contract[] renewedContracts; | |
} | |
public class ProductLoadContext { | |
public Id pricebookId; | |
public String currencyCode; | |
public ProductLoadContext(){} | |
public ProductLoadContext(Id pricebookId, String currencyCode) { | |
this.pricebookId = pricebookId; | |
this.currencyCode = currencyCode; | |
} | |
} | |
public class SearchContext { | |
public String format; | |
public QuoteModel quote; | |
public SBQQ__SearchFilter__c[] filters; | |
} | |
public class SuggestContext { | |
public String format; | |
public QuoteModel quote; | |
public SBQQ__QuoteProcess__c process; | |
} | |
public class ProductAddContext { | |
public Boolean ignoreCalculate; | |
public QuoteModel quote; | |
public ProductModel[] products; | |
public Integer groupKey; | |
public ProductAddContext(){ | |
products = new List<ProductModel>(); | |
} | |
public ProductAddContext(QuoteModel quote, ProductModel[] products){ | |
this(false, quote, products, null); | |
} | |
public ProductAddContext(Boolean ignoreCalculate, QuoteModel quote, ProductModel[] products) { | |
this(ignoreCalculate, quote, products, null); | |
} | |
public ProductAddContext(Boolean ignoreCalculate, QuoteModel quote, ProductModel[] products, Integer groupKey){ | |
this.ignoreCalculate = ignoreCalculate; | |
this.quote = quote; | |
this.products = products; | |
this.groupKey = groupKey; | |
} | |
} | |
public class CalculatorContext { | |
public QuoteModel quote; | |
public CalculatorContext(){} | |
public CalculatorContext(QuoteModel quote) { | |
this.quote = quote; | |
} | |
} | |
public class ConfigLoadContext { | |
public TinyQuoteModel quote; | |
public TinyProductModel parentProduct; // Only required if the configuration must inherit Configuration Attribute values from its parent. | |
} | |
public class LoadRuleRunnerContext { | |
public TinyQuoteModel quote; | |
public String[] dynamicOptionSkus; | |
public TinyConfigurationModel configuration; | |
public TinyProductModel parentProduct; // Only required if the configuration must inherit Configuration Attributes from the parent. | |
} | |
public class ValidationContext { | |
public TinyQuoteModel quote; | |
public TinyConfigurationModel configuration; | |
public Id upgradedAssetId; | |
public String event; | |
} | |
/** DATA MODELS */ | |
public without sharing class ProductModel { | |
/** | |
* The record that this product model represents. | |
*/ | |
public Product2 record {get; private set;} | |
/** | |
* Provides a source for SBQQ__QuoteLine__c.SBQQ__UpgradedAsset__c | |
*/ | |
public Id upgradedAssetId {get; set;} | |
/** | |
* The symbol for the currency in use | |
*/ | |
public String currencySymbol {get; private set;} | |
/** | |
* The ISO code for the currency in use | |
*/ | |
public String currencyCode {get; private set;} | |
/** | |
* Allows for Product Features to be sorted by category | |
*/ | |
public String[] featureCategories {get; private set;} | |
/** | |
* A list of all available options on this product | |
*/ | |
public OptionModel[] options {get; private set;} | |
/** | |
* All features present on this product | |
*/ | |
public FeatureModel[] features {get; private set;} | |
/** | |
* An object representing this product's current configuration | |
*/ | |
public ConfigurationModel configuration {get; private set;} | |
/** | |
* A list of all configuration attributes available on this product | |
*/ | |
public ConfigAttributeModel[] configurationAttributes {get; private set;} | |
/** | |
* A list of all configuration attributes this product inherits from ancestor products | |
*/ | |
public ConfigAttributeModel[] inheritedConfigurationAttributes {get; private set;} | |
/** | |
* Constraints on this product | |
*/ | |
public ConstraintModel[] constraints; | |
} | |
public class ConstraintModel { | |
public SBQQ__OptionConstraint__c record; | |
public Boolean priorOptionExists; | |
} | |
public class OptionModel { | |
public SBQQ__ProductOption__c record; | |
public Map<String,String> externalConfigurationData; | |
public Boolean configurable; | |
public Boolean configurationRequired; | |
public Boolean quantityEditable; | |
public Boolean priceEditable; | |
public Decimal productQuantityScale; | |
public Boolean priorOptionExists; | |
public Set<Id> dependentIds; | |
public Map<String,Set<Id>> controllingGroups; | |
public Map<String,Set<Id>> exclusionGroups; | |
public String reconfigureDimensionWarning; | |
public Boolean hasDimension; | |
public Boolean isUpgrade; | |
public String dynamicOptionKey; | |
} | |
public class ConfigAttributeModel { | |
public String name; | |
public String targetFieldName; // Corresponds directly to SBQQ__ConfigurationAttribute__c.SBQQ__TargetField__c | |
public Decimal displayOrder; // Corresponds directly to SBQQ__ConfigurationAttribute__c.SBQQ__DisplayOrder__c | |
public String columnOrder; // Corresponds directly to SBQQ__ConfigurationAttribute__c.SBQQ__ColumnOrder__c | |
public Boolean required; // Corresponds directly to SBQQ__ConfigurationAttribute__c.SBQQ__Required__c | |
public Id featureId; // Corresponds directly to SBQQ__ConfigurationAttribute__c.SBQQ__Feature__c | |
public String position; // Corresponds directly to SBQQ__ConfigurationAttribute__c.SBQQ__Position__c | |
public Boolean appliedImmediately; // Corresponds directly to SBQQ__ConfigurationAttribute__c.SBQQ__AppliedImmediately__c | |
public Boolean applyToProductOptions; // Corresponds directly to SBQQ__ConfigurationAttribute__c.SBQQ__ApplyToProductOptions__c | |
public Boolean autoSelect; // Corresponds directly to SBQQ__ConfigurationAttribute__c.SBQQ__AutoSelect__c | |
public String[] shownValues; // Corresponds directly to SBQQ__ConfigurationAttribute__c.SBQQ__ShownValues__c | |
public String[] hiddenValues; // Corresponds directly to SBQQ__ConfigurationAttribute__c.SBQQ__HiddenValues__c | |
public Boolean hidden; // Corresponds directly to SBQQ__ConfigurationAttribute__c.SBQQ__Hidden__c | |
public String noSuchFieldName; // If no field with the target name exists, the target name is stored here. | |
public Id myId; // Corresponds directly to SBQQ__ConfigurationAttribute__c.Id | |
} | |
public class FeatureModel { | |
public SBQQ__ProductFeature__c record; | |
public String instructionsText; | |
public Boolean containsUpgrades; | |
} | |
public class ConfigurationModel { | |
public Id configuredProductId; | |
public Id optionId; | |
public SBQQ__ProductOption__c optionData; // Editable data about the option in question, such as quantity or discount | |
public SBQQ__ProductOption__c configurationData; | |
public SBQQ__ProductOption__c inheritedConfigurationData; | |
public ConfigurationModel[] optionConfigurations; | |
public Boolean configured; | |
public Boolean configurationEntered; | |
public Boolean changedByProductActions; | |
public Boolean isDynamicOption; | |
public Boolean isUpgrade; | |
public Set<Id> disabledOptionIds; | |
public Set<Id> hiddenOptionIds; | |
public Decimal listPrice; | |
public Boolean priceEditable; | |
public String[] validationMessages; | |
public String dynamicOptionKey; | |
} | |
public without sharing class QuoteModel { | |
/** | |
* The record represented by this model | |
*/ | |
public SBQQ__Quote__c record; | |
/** | |
* The lines contained in this quote | |
*/ | |
public QuoteLineModel[] lineItems; | |
/** | |
* The groups contained in this quote | |
*/ | |
public QuoteLineGroupModel[] lineItemGroups; | |
/** | |
* The next key that will be used for new groups or lines. | |
* To ensure uniqueness of keys, this value should never be changed to a lower value. | |
*/ | |
public Integer nextKey; | |
/** | |
* Corresponds to the 'magic field', SBQQ__Quote__c.ApplyAdditionalDiscountLast__c | |
*/ | |
public Boolean applyAdditionalDiscountLast; | |
/** | |
* Corresponds to the 'magic field', SBQQ__Quote__c.ApplyPartnerDiscountFirst__c | |
*/ | |
public Boolean applyPartnerDiscountFirst; | |
/** | |
* Corresponds to the 'magic field', SBQQ__Quote__c.ChannelDiscountsOffList__c | |
*/ | |
public Boolean channelDiscountsOffList; | |
/** | |
* SBQQ__Quote__c.SBQQ__CustomerAmount__c is a Roll-up Summary Field, so its accuracy can only be guaranteed | |
* after a quote is persisted. As such, its current value is stored here until then. | |
*/ | |
public Decimal customerTotal; | |
/** | |
* SBQQ__Quote__c.SBQQ__NetAmount__c is a Roll-up Summary Field, so its accuracy can only be guaranteed | |
* after a quote is persisted. As such, its current value is stored here until then. | |
*/ | |
public Decimal netTotal; | |
/** | |
* The Net Total for all non-multidimensional quote lines. | |
*/ | |
public Decimal netNonSegmentTotal; | |
public Boolean calculationRequired; | |
} | |
public without sharing class QuoteLineModel { | |
/** | |
* The record represented by this model. | |
*/ | |
public SBQQ__QuoteLine__c record; | |
/** | |
* Corresponds to the 'magic field', SBQQ__QuoteLine__c.ProrateAmountDiscount__c. | |
*/ | |
public Boolean amountDiscountProrated; | |
/** | |
* The unique key of this line's group, if this line is part of a grouped quote. | |
*/ | |
public Integer parentGroupKey; | |
/** | |
* The unique key of this line's parent, if this line is part of a bundle. | |
*/ | |
public Integer parentItemKey; | |
/** | |
* Each quote line and group has a key that is unique against all other keys on the same quote. | |
*/ | |
public Integer key; | |
/** | |
* True if this line is an MDQ segment that can be uplifted from a previous segment. | |
*/ | |
public Boolean upliftable; | |
/** | |
* Indicates the configuration type of the product this line represents. | |
*/ | |
public String configurationType; | |
/** | |
* Indicates the configuration event of the product this line represents. | |
*/ | |
public String configurationEvent; | |
/** | |
* If true, this line cannot be reconfigured. | |
*/ | |
public Boolean reconfigurationDisabled; | |
/** | |
* If true, this line's description cannot be changed. | |
*/ | |
public Boolean descriptionLocked; | |
/** | |
* If true, this line's quantity cannot be changed. | |
*/ | |
public Boolean productQuantityEditable; | |
/** | |
* The number of decimal places to which this line's quantity shall be rounded. | |
*/ | |
public Decimal productQuantityScale; | |
/** | |
* The type of MDQ dimension this line represents. | |
*/ | |
public String dimensionType; | |
/** | |
* If true, the underlying product can be represented as a Multi-dimensional line. | |
*/ | |
public Boolean productHasDimensions; | |
/** | |
* The unit price towards which this quote line will be discounted. | |
*/ | |
public Decimal targetCustomerAmount; | |
/** | |
* The customer amount towards which this quote line will be discounted. | |
*/ | |
public Decimal targetCustomerTotal; | |
/** | |
* The net total towards which this quote line will be discounted. | |
*/ | |
} | |
public without sharing class QuoteLineGroupModel { | |
/** | |
* The record represented by this model. | |
*/ | |
public SBQQ__QuoteLineGroup__c record; | |
/** | |
* The Net Total for all non-multidimensional quote lines. | |
*/ | |
public Decimal netNonSegmentTotal; | |
/** | |
* Each quote line and group has a key that is unique against all other keys on the same quote. | |
*/ | |
public Integer key; | |
} | |
// ============ TINY MODEL CLASSES ========= | |
// Use these with config API's | |
// These are referenced in the docs here: https://community.steelbrick.com/t5/Developer-Guidebook/Public-API-Technical-Documentation-amp-Code-2/ta-p/5691 | |
// They should probably be refactored to use full models (even if some values are null) instead of keeping multiple versions | |
public class TinyProductModel { | |
public Product2 record; | |
public String currencyCode; | |
public TinyOptionModel[] options; | |
public TinyFeatureModel[] features; | |
public TinyConfigurationModel configuration; | |
public TinyConfigAttributeModel[] configurationAttributes; | |
public TinyConfigAttributeModel[] inheritedConfigurationAttributes; | |
public TinyConstraintModel[] constraints; | |
} | |
public class TinyConstraintModel { | |
public SBQQ__OptionConstraint__c record; | |
public Boolean priorOptionExists; | |
} | |
public class TinyOptionModel { | |
public SBQQ__ProductOption__c record; | |
public Map<String,String> externalConfigurationData; | |
public Boolean configurable; | |
public Boolean configurationRequired; | |
public Boolean quantityEditable; | |
public Boolean priceEditable; | |
public Decimal productQuantityScale; | |
public Boolean priorOptionExists; | |
public Set<Id> dependentIds; | |
public Map<String,Set<Id>> controllingGroups; | |
public Map<String,Set<Id>> exclusionGroups; | |
public String reconfigureDimensionWarning; | |
public Boolean hasDimension; | |
public Boolean isUpgrade; | |
public String dynamicOptionKey; | |
} | |
public class TinyConfigAttributeModel { | |
public String name; | |
public String targetFieldName; // Corresponds directly to SBQQ__ConfigurationAttribute__c.SBQQ__TargetField__c | |
public Decimal displayOrder; // Corresponds directly to SBQQ__ConfigurationAttribute__c.SBQQ__DisplayOrder__c | |
public String columnOrder; // Corresponds directly to SBQQ__ConfigurationAttribute__c.SBQQ__ColumnOrder__c | |
public Boolean required; // Corresponds directly to SBQQ__ConfigurationAttribute__c.SBQQ__Required__c | |
public Id featureId; // Corresponds directly to SBQQ__ConfigurationAttribute__c.SBQQ__Feature__c | |
public String position; // Corresponds directly to SBQQ__ConfigurationAttribute__c.SBQQ__Position__c | |
public Boolean appliedImmediately; // Corresponds directly to SBQQ__ConfigurationAttribute__c.SBQQ__AppliedImmediately__c | |
public Boolean applyToProductOptions; // Corresponds directly to SBQQ__ConfigurationAttribute__c.SBQQ__ApplyToProductOptions__c | |
public Boolean autoSelect; // Corresponds directly to SBQQ__ConfigurationAttribute__c.SBQQ__AutoSelect__c | |
public String[] shownValues; // Corresponds directly to SBQQ__ConfigurationAttribute__c.SBQQ__ShownValues__c | |
public String[] hiddenValues; // Corresponds directly to SBQQ__ConfigurationAttribute__c.SBQQ__HiddenValues__c | |
public Boolean hidden; // Corresponds directly to SBQQ__ConfigurationAttribute__c.SBQQ__Hidden__c | |
public String noSuchFieldName; // If no field with the target name exists, the target name is stored here. | |
public Id myId; // Corresponds directly to SBQQ__ConfigurationAttribute__c.Id | |
} | |
public class TinyFeatureModel { | |
public SBQQ__ProductFeature__c record; | |
public String instructionsText; | |
public Boolean containsUpgrades; | |
} | |
public class TinyConfigurationModel { | |
public Id configuredProductId; | |
public Id optionId; | |
public SBQQ__ProductOption__c optionData; // Editable data about the option in question, such as quantity or discount | |
public SBQQ__ProductOption__c configurationData; | |
public SBQQ__ProductOption__c inheritedConfigurationData; | |
public TinyConfigurationModel[] optionConfigurations; | |
public Boolean configured; | |
public Boolean changedByProductActions; | |
public Boolean isDynamicOption; | |
public Boolean isUpgrade; | |
public Set<Id> disabledOptionIds; | |
public Set<Id> hiddenOptionIds; | |
public Decimal listPrice; | |
public Boolean priceEditable; | |
public String[] validationMessages; | |
public String dynamicOptionKey; | |
} | |
public class TinyQuoteModel { | |
public SBQQ__Quote__c record; | |
public TinyQuoteLineModel[] lineItems; | |
public TinyQuoteLineGroupModel[] lineItemGroups; | |
public Integer nextKey; | |
public Boolean applyAdditionalDiscountLast; | |
public Boolean applyPartnerDiscountFirst; | |
public Boolean channelDiscountsOffList; | |
public Decimal customerTotal; | |
public Decimal netTotal; | |
public Decimal netNonSegmentTotal; | |
} | |
public class TinyQuoteLineModel { | |
public SBQQ__QuoteLine__c record; | |
public Decimal renewalPrice; | |
public Boolean amountDiscountProrated; | |
public Integer parentGroupKey; | |
public Integer parentItemKey; | |
public Integer key; | |
public Boolean upliftable; | |
public String configurationType; | |
public String configurationEvent; | |
public Boolean reconfigurationDisabled; | |
public Boolean descriptionLocked; | |
public Boolean productQuantityEditable; | |
public Decimal productQuantityScale; | |
public String dimensionType; | |
public Boolean productHasDimensions; | |
public Decimal targetCustomerAmount; | |
public Decimal targetCustomerTotal; | |
} | |
public class TinyQuoteLineGroupModel { | |
public SBQQ__QuoteLineGroup__c record; | |
public Decimal netNonSegmentTotal; | |
public Integer key; | |
} | |
} |
/** | |
* This test class is only testing for code coverage. Functionality does not require testing for this wrapper class. | |
* | |
*/ | |
@isTest | |
private class CPQ_ApiDataModelsTest { | |
@isTest static void testProductLoadContext() { | |
CPQ_ApiDataModels.ProductLoadContext loadContext = new CPQ_ApiDataModels.ProductLoadContext(); | |
System.assertEquals(loadContext.pricebookId, null); | |
System.assertEquals(loadContext.currencyCode, null); | |
Id pricebookId = Test.getStandardPricebookId(); | |
String currencyCode = 'USD'; | |
CPQ_ApiDataModels.ProductLoadContext loadContextWithPricebookAndCurrency = new CPQ_ApiDataModels.ProductLoadContext(pricebookId, currencyCode); | |
System.assertEquals(loadContextWithPricebookAndCurrency.pricebookId, pricebookId); | |
System.assertEquals(loadContextWithPricebookAndCurrency.currencyCode, currencyCode); | |
} | |
@isTest static void testProductAddContext() { | |
CPQ_ApiDataModels.ProductAddContext addContextDefault = new CPQ_ApiDataModels.ProductAddContext(); | |
System.assertEquals(addContextDefault.quote, null); | |
System.assertEquals(addContextDefault.products, new List<CPQ_ApiDataModels.ProductModel>()); | |
System.assertEquals(addContextDefault.groupKey, null); | |
CPQ_ApiDataModels.QuoteModel quote = new CPQ_ApiDataModels.QuoteModel(); | |
List<CPQ_ApiDataModels.ProductModel> products = new List<CPQ_ApiDataModels.ProductModel>(); | |
CPQ_ApiDataModels.ProductAddContext addContextquoteProducts = new CPQ_ApiDataModels.ProductAddContext(quote, products); | |
System.assertEquals(addContextquoteProducts.quote, quote); | |
System.assertEquals(addContextquoteProducts.products, products); | |
System.assertEquals(addContextquoteProducts.groupKey, null); | |
CPQ_ApiDataModels.ProductAddContext addContextquoteProductsIgnoreCalculate = new CPQ_ApiDataModels.ProductAddContext(true, quote, products); | |
System.assertEquals(addContextquoteProductsIgnoreCalculate.quote, quote); | |
System.assertEquals(addContextquoteProductsIgnoreCalculate.products, products); | |
System.assertEquals(addContextquoteProductsIgnoreCalculate.groupKey, null); | |
System.assertEquals(addContextquoteProductsIgnoreCalculate.ignoreCalculate, true); | |
CPQ_ApiDataModels.ProductAddContext addContextquoteProductsIgnoreCalculateWithGroup = | |
new CPQ_ApiDataModels.ProductAddContext(true, quote, products, 1); | |
System.assertEquals(addContextquoteProductsIgnoreCalculateWithGroup.quote, quote); | |
System.assertEquals(addContextquoteProductsIgnoreCalculateWithGroup.products, products); | |
System.assertEquals(addContextquoteProductsIgnoreCalculateWithGroup.groupKey, 1); | |
System.assertEquals(addContextquoteProductsIgnoreCalculateWithGroup.ignoreCalculate, true); | |
} | |
@isTest static void testCalculatorContext() { | |
CPQ_ApiDataModels.CalculatorContext calcContext = new CPQ_ApiDataModels.CalculatorContext(); | |
System.assertEquals(calcContext.quote, null); | |
CPQ_ApiDataModels.QuoteModel quote = new CPQ_ApiDataModels.QuoteModel(); | |
CPQ_ApiDataModels.CalculatorContext calcContextWithQuote = new CPQ_ApiDataModels.CalculatorContext(quote); | |
System.assertEquals(calcContextWithQuote.quote, quote); | |
} | |
@isTest static void testProductModel() { | |
String productModelJson = '{' + | |
'"record": {' + | |
'"attributes": {' + | |
'"type": "Product2",' + | |
'"url": "/services/data/v42.0/sobjects/Product2/01t0q000000gaO9AAI"' + | |
'},' + | |
'"Id": "01t0q000000gaO9AAI",' + | |
'"CurrencyIsoCode": "USD",' + | |
'"Name": "API - Overage",' + | |
'"ProductCode": "API - Overage",' + | |
'"Description": "atg",' + | |
'"SBQQ__SubscriptionPricing__c": "Fixed Price",' + | |
'"SBQQ__PriceEditable__c": false,' + | |
'"SBQQ__DefaultQuantity__c": 1.00000,' + | |
'"SBQQ__QuantityEditable__c": true,' + | |
'"SBQQ__CostEditable__c": false,' + | |
'"SBQQ__NonDiscountable__c": false,' + | |
'"SBQQ__NonPartnerDiscountable__c": false,' + | |
'"SBQQ__SubscriptionTerm__c": 1,' + | |
'"SBQQ__PricingMethod__c": "List",' + | |
'"SBQQ__PricingMethodEditable__c": true,' + | |
'"SBQQ__OptionSelectionMethod__c": "Click",' + | |
'"SBQQ__Optional__c": false,' + | |
'"SBQQ__Taxable__c": false,' + | |
'"SBQQ__CustomConfigurationRequired__c": false,' + | |
'"SBQQ__Hidden__c": false,' + | |
'"SBQQ__ReconfigurationDisabled__c": false,' + | |
'"SBQQ__ExcludeFromOpportunity__c": true,' + | |
'"SBQQ__DescriptionLocked__c": false,' + | |
'"SBQQ__ExcludeFromMaintenance__c": false,' + | |
'"SBQQ__IncludeInMaintenance__c": false,' + | |
'"SBQQ__AllocatePotOnOrders__c": false,' + | |
'"SBQQ__NewQuoteGroup__c": false,' + | |
'"SBQQ__SubscriptionType__c": "Renewable",' + | |
'"SBQQ__HasConfigurationAttributes__c": false,' + | |
'"SBQQ__ExternallyConfigurable__c": false,' + | |
'"SBQQ__BillingFrequency__c": "Monthly",' + | |
'"SBQQ__ChargeType__c": "Usage",' + | |
'"PricebookEntries": {' + | |
'"totalSize": 1,' + | |
'"done": true,' + | |
'"records": [' + | |
'{' + | |
'"attributes": {' + | |
'"type": "PricebookEntry",' + | |
'"url": "/services/data/v42.0/sobjects/PricebookEntry/01u0q000001jwBjAAI"' + | |
'},' + | |
'"Product2Id": "01t0q000000gaO9AAI",' + | |
'"Id": "01u0q000001jwBjAAI",' + | |
'"Pricebook2Id": "01s0q000000CbjqAAC",' + | |
'"UnitPrice": 0.08,' + | |
'"IsActive": true,' + | |
'"CurrencyIsoCode": "USD"' + | |
'}' + | |
']' + | |
'}' + | |
'},' + | |
'"options": [],' + | |
'"features": [],' + | |
'"featureCategoryLabels": {' + | |
'"Reporting": "Reporting",' + | |
'"Implementation": "Implementation",' + | |
'"Software": "Software",' + | |
'"Hardware": "Hardware"' + | |
'},' + | |
'"featureCategories": [],' + | |
'"currencySymbol": "USD",' + | |
'"currencyCode": "USD",' + | |
'"constraints": [],' + | |
'"configurationAttributes": []' + | |
'}'; | |
CPQ_ApiDataModels.ProductModel productModel = (CPQ_ApiDataModels.ProductModel) JSON.deserialize(productModelJson, CPQ_ApiDataModels.ProductModel.class); | |
System.assertEquals(productModel.record.Name, 'API - Overage'); | |
System.assertEquals(productModel.upgradedAssetId, null); | |
System.assertEquals(productModel.currencySymbol, 'USD'); | |
System.assertEquals(productModel.currencyCode, 'USD'); | |
System.assertEquals(productModel.featureCategories, new String[]{}); | |
System.assertEquals(productModel.options, new CPQ_ApiDataModels.OptionModel[]{}); | |
System.assertEquals(productModel.features, new CPQ_ApiDataModels.FeatureModel[]{}); | |
System.assertEquals(productModel.configuration, null); | |
System.assertEquals(productModel.configurationAttributes, new CPQ_ApiDataModels.ConfigAttributeModel[]{}); | |
System.assertEquals(productModel.inheritedConfigurationAttributes, null); | |
} | |
} |
/** | |
* | |
* | |
* @description This class wraps the Salesforce CPQ API to allow | |
* easier interaction and to demonstrate how to call various methods | |
* EXAMPLE INVOKING: | |
* Contract contract = [SELECT Id FROM Contract WHERE Id = '800f4000000DL11' LIMIT 1]; | |
* CPQ_ApiWrapper.renewContract(contract); | |
* To Manually call the CPQ API via REST, the convention is as follows: | |
* GET /services/apexrest/SBQQ/ServiceRouter/read?reader=SBQQ.QuoteAPI.QuoteReader&uid=a0nf4000000W4vs | |
* | |
*/ | |
public without sharing class CPQ_ApiWrapper { | |
public static Boolean debug = true; | |
/** CPQ API METHODS */ | |
public static final String CONTRACT_RENEWER = 'SBQQ.ContractManipulationAPI.ContractRenewer'; | |
public static final String CONTRACT_AMENDER = 'SBQQ.ContractManipulationAPI.ContractAmender'; | |
public static final String CONFIG_LOADER = 'SBQQ.ConfigAPI.ConfigLoader'; | |
public static final String LOAD_RULE_EXECUTOR = 'SBQQ.ConfigAPI.LoadRuleExecutor'; | |
public static final String CONFIGURATION_VALIDATOR = 'SBQQ.ConfigAPI.ConfigurationValidator'; | |
public static final String PRODUCT_LOADER = 'SBQQ.ProductAPI.ProductLoader'; | |
public static final String PRODUCT_SUGGESTER = 'SBQQ.ProductAPI.ProductSuggester'; | |
public static final String PRODUCT_SEARCHER = 'SBQQ.ProductAPI.ProductSearcher'; | |
public static final String QUOTE_READER = 'SBQQ.QuoteAPI.QuoteReader'; | |
public static final String QUOTE_PRODUCT_ADDER = 'SBQQ.QuoteAPI.QuoteProductAdder'; | |
public static final String QUOTE_CALCULATOR = 'SBQQ.QuoteAPI.QuoteCalculator'; | |
public static final String QUOTE_SAVER = 'SBQQ.QuoteAPI.QuoteSaver'; | |
/** Mini Wrapper around SBQQ API METHODS */ | |
private static String read(String name, String uid) { | |
return SBQQ.ServiceRouter.read(name, uid); | |
} | |
private static String load(String name, String uid, Object payload) { | |
return loadStr(name, uid, JSON.serialize(payload)); | |
} | |
private static String loadStr(String name, String uid, String payloadJson) { | |
return SBQQ.ServiceRouter.load(name, uid, payloadJson); | |
} | |
private static String save(String name, Object model) { | |
return saveStr(name, JSON.serialize(model)); | |
} | |
private static String saveStr(String name, String modelJson) { | |
return SBQQ.ServiceRouter.save(name, modelJson); | |
} | |
// Will need to add unit tests for these if uncommented | |
//public static List<CPQ_ApiDataModels.QuoteModel> renewContract(Contract contract) { | |
// return renewContract(contract.Id, new List<Contract>{contract}); | |
//} | |
//public static List<CPQ_ApiDataModels.QuoteModel> renewContract(Id contractId, List<Contract> contracts) { | |
// CPQ_ApiDataModels.RenewalContext payload = new CPQ_ApiDataModels.RenewalContext(); | |
// payload.renewedContracts = contracts; | |
// String jsonResult = load(CONTRACT_RENEWER, (String) contractId, payload); | |
// if(debug) { | |
// System.debug(LoggingLevel.WARN, 'jsonResult: ' + jsonResult); | |
// } | |
// List<CPQ_ApiDataModels.QuoteModel> quoteModel = (List<CPQ_ApiDataModels.QuoteModel>) JSON.deserialize(jsonResult, LIST<CPQ_ApiDataModels.QuoteModel>.class); | |
// if(debug) { | |
// System.debug(LoggingLevel.WARN, 'jsonResult: ' + jsonResult); | |
// System.debug(LoggingLevel.WARN, 'quoteModel: ' + quoteModel); | |
// } | |
// return quoteModel; | |
//} | |
//public static CPQ_ApiDataModels.QuoteModel amendContract(Id contractId) { | |
// System.debug(LoggingLevel.WARN, 'amending'); | |
// String jsonResult = load(CONTRACT_AMENDER, (String) contractId, null); | |
// System.debug(LoggingLevel.WARN, 'amended ' + jsonResult); | |
// CPQ_ApiDataModels.QuoteModel quoteModel = (CPQ_ApiDataModels.QuoteModel) JSON.deserialize(jsonResult, CPQ_ApiDataModels.QuoteModel.class); | |
// System.debug(LoggingLevel.WARN, 'quoteModel >>> ' + quoteModel); | |
//if(debug) { | |
// System.debug(LoggingLevel.WARN, 'jsonResult: ' + jsonResult); | |
// System.debug(LoggingLevel.WARN, 'quoteModel: ' + quoteModel); | |
//} | |
//return quoteModel; | |
//} | |
/** | |
* ******* QUOTE API EXAMPLES ******** | |
*/ | |
public static CPQ_ApiDataModels.QuoteModel getQuoteModel(Id quoteId) { | |
String jsonResult = read(QUOTE_READER, (String) quoteId); | |
CPQ_ApiDataModels.QuoteModel quoteModel = (CPQ_ApiDataModels.QuoteModel) JSON.deserialize(jsonResult, CPQ_ApiDataModels.QuoteModel.class); | |
if(debug) { | |
System.debug(LoggingLevel.WARN, 'jsonResult: ' + jsonResult); | |
System.debug(LoggingLevel.WARN, 'quoteModel: ' + quoteModel); | |
} | |
return quoteModel; | |
} | |
public static CPQ_ApiDataModels.ProductModel loadProduct(Id productId, Id pricebookId, String currencyCode) { | |
CPQ_ApiDataModels.ProductLoadContext productLoadPayload = new CPQ_ApiDataModels.ProductLoadContext(pricebookId, currencyCode); | |
String jsonResultProduct = load(PRODUCT_LOADER, (String) productId, productLoadPayload); | |
CPQ_ApiDataModels.ProductModel productModel = (CPQ_ApiDataModels.ProductModel) JSON.deserialize(jsonResultProduct, CPQ_ApiDataModels.ProductModel.class); | |
if(debug) { | |
System.debug(LoggingLevel.WARN, 'jsonResultProduct: ' + jsonResultProduct); | |
System.debug(LoggingLevel.WARN, 'productModel: ' + productModel); | |
} | |
return productModel; | |
} | |
public static CPQ_ApiDataModels.ProductModel setOptionsConfigured(CPQ_ApiDataModels.ProductModel productModel) { | |
if(productModel.configuration != null){ | |
productModel.configuration.configured = true; | |
productModel.configuration.configurationEntered = true; | |
for(CPQ_ApiDataModels.ConfigurationModel configModel : productModel.configuration.optionConfigurations) { | |
configModel.configured = true; | |
configModel.configurationEntered = true; | |
} | |
return productModel; | |
}else{return productModel;} | |
} | |
public static CPQ_ApiDataModels.QuoteModel addProductsToQuote(Id quoteId, Id productId, Id pricebookId, String currencyCode) { | |
return addProductsToQuote(quoteId, pricebookId, productId, currencyCode, false); | |
} | |
public static CPQ_ApiDataModels.QuoteModel addProductsToQuote(Id quoteId, Id productId, Id pricebookId, String currencyCode, Boolean skipCalculate) { | |
CPQ_ApiDataModels.ProductModel productModel = loadProduct(productId, pricebookId, currencyCode); | |
// Set product model as configured and configurationEntered | |
productModel = setOptionsConfigured(productModel); | |
String jsonResultQuote = read(QUOTE_READER, (String) quoteId); | |
CPQ_ApiDataModels.QuoteModel initialQuoteModel = (CPQ_ApiDataModels.QuoteModel) JSON.deserialize(jsonResultQuote, CPQ_ApiDataModels.QuoteModel.class); | |
if(debug) { | |
System.debug(LoggingLevel.WARN, 'jsonResultQuote: ' + jsonResultQuote); | |
System.debug(LoggingLevel.WARN, 'initialQuoteModel: ' + initialQuoteModel); | |
} | |
CPQ_ApiDataModels.ProductAddContext productAddPayload = new CPQ_ApiDataModels.ProductAddContext(skipCalculate, initialQuoteModel, new List<CPQ_ApiDataModels.ProductModel>{productModel}); | |
return addProductsToQuote(productAddPayload); | |
} | |
public static CPQ_ApiDataModels.QuoteModel addProductsToQuote(CPQ_ApiDataModels.ProductAddContext productAddPayload) { | |
if(debug) { | |
System.debug(LoggingLevel.WARN, 'productAddPayloadJSON: ' + JSON.serialize(productAddPayload)); | |
} | |
String updatedQuoteJSON = load(QUOTE_PRODUCT_ADDER, null, productAddPayload); | |
CPQ_ApiDataModels.QuoteModel updatedQuoteModel = (CPQ_ApiDataModels.QuoteModel) JSON.deserialize(updatedQuoteJSON, CPQ_ApiDataModels.QuoteModel.class); | |
if(debug) { | |
System.debug(LoggingLevel.WARN, 'updatedQuoteJSON: ' + updatedQuoteJSON); | |
System.debug(LoggingLevel.WARN, 'updatedQuoteModel: ' + updatedQuoteModel); | |
} | |
return updatedQuoteModel; | |
} | |
public static CPQ_ApiDataModels.QuoteModel calculateQuote(CPQ_ApiDataModels.QuoteModel quoteModel) { | |
CPQ_ApiDataModels.CalculatorContext calculatorPayload = new CPQ_ApiDataModels.CalculatorContext(quoteModel); | |
String updatedQuoteJSON = load(QUOTE_CALCULATOR, null, calculatorPayload); | |
CPQ_ApiDataModels.QuoteModel updatedQuoteModel = (CPQ_ApiDataModels.QuoteModel) JSON.deserialize(updatedQuoteJSON, CPQ_ApiDataModels.QuoteModel.class); | |
if(debug) { | |
System.debug(LoggingLevel.WARN, 'updatedQuoteJSON: ' + updatedQuoteJSON); | |
System.debug(LoggingLevel.WARN, 'updatedQuoteModel: ' + updatedQuoteModel); | |
} | |
return updatedQuoteModel; | |
} | |
public static CPQ_ApiDataModels.QuoteModel saveQuote(CPQ_ApiDataModels.QuoteModel quoteModel) { | |
String savedQuoteJSON = save(QUOTE_SAVER, quoteModel); | |
CPQ_ApiDataModels.QuoteModel updatedQuoteModel = (CPQ_ApiDataModels.QuoteModel) JSON.deserialize(savedQuoteJSON, CPQ_ApiDataModels.QuoteModel.class); | |
if(debug) { | |
System.debug(LoggingLevel.WARN, 'updatedQuoteModel: ' + updatedQuoteModel); | |
} | |
return updatedQuoteModel; | |
} | |
public static CPQ_ApiDataModels.QuoteModel calculateAndSaveQuote(CPQ_ApiDataModels.QuoteModel quoteModel) { | |
//String calculatedQuoteJSON = SBQQ.QuoteLineEditorController.calculateQuote2(quoteModel.record.Id, JSON.serialize(quoteModel)); | |
// Attempt to get around uncomitted changes by saving first | |
String savedQuoteJSON = saveStr(QUOTE_SAVER, JSON.serialize(quoteModel)); | |
//String calculatedQuoteJSON = SBQQ.QuoteLineEditorController.calculateQuote2(quoteModel.record.Id, JSON.serialize(savedQuoteJSON)); | |
//savedQuoteJSON = saveStr(QUOTE_SAVER, calculatedQuoteJSON); | |
CPQ_ApiDataModels.QuoteModel savedQuote = (CPQ_ApiDataModels.QuoteModel) JSON.deserialize(savedQuoteJSON, CPQ_ApiDataModels.QuoteModel.class); | |
//String calculatedQuoteJSON = SBQQ.QuoteLineEditorController.calculateQuote2(quoteModel.record.Id, JSON.serialize(savedQuoteJSON)); | |
if(debug) { | |
//System.debug(LoggingLevel.WARN, 'calculatedQuoteJSON: ' + calculatedQuoteJSON); | |
System.debug(LoggingLevel.WARN, 'savedQuoteJSON: ' + savedQuoteJSON); | |
//System.debug(LoggingLevel.WARN, 'savedQuote: ' + savedQuote); | |
} | |
return savedQuote; | |
} | |
//public static void configureBundle(CPQ_ApiDataModels.QuoteModel quoteModel, Id productId) { | |
// //CPQ_ApiDataModels.ConfigLoadContext context = new CPQ_ApiDataModels.ConfigLoadContext(); | |
// // Using alt payload to avoid requiring tiny quoteModel since it is basically the same - could modify contrstructor to convert | |
// CPQ_ApiDataModels.TinyQuoteModel tinyQuoteModel = new CPQ_ApiDataModels.TinyQuoteModel(); | |
// tinyQuoteModel.record = quoteModel.record; | |
// CPQ_ApiDataModels.ConfigLoadContext context = new CPQ_ApiDataModels.ConfigLoadContext(); | |
// context.quote = tinyQuoteModel; | |
// System.debug(LoggingLevel.WARN, JSON.serialize(context)); | |
// String configLoaderJSON = load(CONFIG_LOADER, (String) productId, JSON.serialize(context)); | |
// if(debug) { | |
// System.debug(LoggingLevel.WARN, 'configLoaderJSON: ' + configLoaderJSON); | |
// } | |
//} | |
/** | |
* Force a re-calculation of provided quote | |
* @param quoteId [description] | |
* @return [description] | |
*/ | |
public static CPQ_ApiDataModels.QuoteModel calculateQuote(String quoteId) { | |
CPQ_ApiDataModels.QuoteModel initialQuoteModel = getQuoteModel(quoteId); | |
if(debug) { | |
System.debug(LoggingLevel.WARN, 'initialQuoteModel: ' + initialQuoteModel); | |
} | |
CPQ_ApiDataModels.QuoteModel calculatedQuoteModel = calculateQuote(initialQuoteModel); | |
if(debug) { | |
System.debug(LoggingLevel.WARN, 'calculatedQuoteModel: ' + calculatedQuoteModel); | |
} | |
CPQ_ApiDataModels.QuoteModel savedQuoteModel = saveQuote(calculatedQuoteModel); | |
if(debug) { | |
System.debug(LoggingLevel.WARN, 'savedQuoteModel: ' + savedQuoteModel); | |
} | |
return savedQuoteModel; | |
} | |
} |
/** | |
* This test class is only testing for code coverage. Functionality does not require testing for this wrapper class. | |
* | |
*/ | |
@IsTest | |
public class CPQ_ApiWrapperTest { | |
@TestSetup | |
static void setup() { | |
Test.startTest(); | |
CPQ_TestUtils.TestData testData = new CPQ_TestUtils.TestData(); | |
Test.stopTest(); | |
testData.createQuoteData(); | |
} | |
@IsTest | |
public static void testGetQuoteModel() { | |
SBQQ__Quote__c quote = [Select Id FROM SBQQ__Quote__c LIMIT 1]; | |
CPQ_ApiDataModels.QuoteModel quoteModel = CPQ_ApiWrapper.getQuoteModel(quote.Id); | |
} | |
@IsTest | |
public static void testLoadProduct() { | |
Product2 product = CPQ_TestUtils.createProduct(); | |
CPQ_ApiDataModels.ProductModel productModel = CPQ_ApiWrapper.loadProduct(product.Id, Test.getStandardPricebookId(), 'USD'); | |
} | |
@IsTest | |
public static void testAddProductToQuoteAndSaveQuote() { | |
Test.startTest(); | |
SBQQ__Quote__c quote = [Select Id FROM SBQQ__Quote__c LIMIT 1]; | |
Product2 product = [Select Id FROM Product2 LIMIT 1]; | |
CPQ_ApiDataModels.QuoteModel quoteModel = CPQ_ApiWrapper.addProductsToQuote(quote.Id, product.Id, Test.getStandardPricebookId(), 'USD', true); | |
CPQ_ApiWrapper.saveQuote(quoteModel); | |
CPQ_ApiWrapper.calculateQuote(quoteModel); | |
CPQ_ApiWrapper.calculateQuote(quote.Id); | |
CPQ_ApiWrapper.calculateAndSaveQuote(quoteModel); | |
Test.stopTest(); | |
} | |
/** Tests for the various calculator methods available */ | |
//@IsTest | |
//public static void testCalculate() { | |
// //CPQ_TestUtils.TestData testData = new CPQ_TestUtils.TestData(); | |
// //CPQ_ApiDataModels.QuoteModel quoteModel = CPQ_ApiWrapper.getQuoteModel(testData.quote.Id); | |
// //// Theses make a callout and will fail, wrapping in try/catch just for code coverage | |
// //// because it is SBQQ code, not our code and we have functionally tested everything needed | |
// //Test.startTest(); | |
// //try { CPQ_ApiWrapper.calculateQuote(quoteModel); } catch(Exception ex) { } | |
// //Test.stopTest(); | |
//} | |
//@IsTest | |
//public static void testCalculate1() { | |
// //CPQ_TestUtils.TestData testData = new CPQ_TestUtils.TestData(); | |
// //// Theses make a callout and will fail, wrapping in try/catch just for code coverage | |
// //// because it is SBQQ code, not our code and we have functionally tested everything needed | |
// //Test.startTest(); | |
// //try { CPQ_ApiWrapper.calculateQuote(testData.quote.Id); } catch(Exception ex) { } | |
// //Test.stopTest(); | |
//} | |
//@IsTest | |
//public static void testCalculate2() { | |
// //CPQ_TestUtils.TestData testData = new CPQ_TestUtils.TestData(); | |
// //CPQ_ApiDataModels.QuoteModel quoteModel = CPQ_ApiWrapper.getQuoteModel(testData.quote.Id); | |
// //// Theses make a callout and will fail, wrapping in try/catch just for code coverage | |
// //// because it is SBQQ code, not our code and we have functionally tested everything needed | |
// //Test.startTest(); | |
// //try { CPQ_ApiWrapper.calculateAndSaveQuote(quoteModel); } catch(Exception ex) { } | |
// //Test.stopTest(); | |
//} | |
//@IsTest | |
//public static void testCalculate3() { | |
// //CPQ_TestUtils.TestData testData = new CPQ_TestUtils.TestData(); | |
// //// Theses make a callout and will fail, wrapping in try/catch just for code coverage | |
// //// because it is SBQQ code, not our code and we have functionally tested everything needed | |
// //Test.startTest(); | |
// //try { CPQ_ApiWrapper.calculateQuote2(testData.quote.Id); } catch(Exception ex) { } | |
// //Test.stopTest(); | |
//} | |
//@IsTest | |
//public static void testCalculate4() { | |
// //CPQ_TestUtils.TestData testData = new CPQ_TestUtils.TestData(); | |
// ////CPQ_ApiDataModels.QuoteModel quoteModel = CPQ_ApiWrapper.getQuoteModel(testData.quote.Id); | |
// //// Theses make a callout and will fail, wrapping in try/catch just for code coverage | |
// //// because it is SBQQ code, not our code and we have functionally tested everything needed | |
// //Test.startTest(); | |
// //try { CPQ_ApiWrapper.calculateQuote2(quoteModel); } catch(Exception ex) { } | |
// //Test.stopTest(); | |
//} | |
//@IsTest | |
//public static void testAmendContract() { | |
// CPQ_ApiWrapper.debug = true; | |
// Test.startTest(); | |
// Opportunity opp = [SELECT Id, SBQQ__Contracted__c FROM Opportunity LIMIT 1]; | |
// opp.SBQQ__Contracted__c = true; | |
// update opp; | |
// Test.stopTest(); | |
// Contract contract = [SELECT Id FROM Contract LIMIT 1]; | |
// contract.Status = 'Activated'; | |
// update contract; | |
// CPQ_ApiDataModels.QuoteModel quoteModel = CPQ_ApiWrapper.amendContract(contract.Id); | |
// List<SBQQ__Quote__c> quotes = [SELECT Id FROM SBQQ__Quote__c]; | |
// System.assertNotEquals(null, quoteModel); | |
// System.assertEquals(2, quotes.size()); | |
//} | |
} |
@rgonola - These are pretty difficult to troubleshoot. Normally the cause (AFAIK) is that one of your Ids is invalid - ex. Are you 100% sure you are using the correct productId, make sure that the pricebook id you provided contains a pricebook entry for the product in question for the currency you provided.
Aside from that, I am not really sure what other advice I can give.
Good luck!
@pausint can we modify bundle products using this APIs? Here is our scenario.
Salesrep configure initial bundle in Salesforce CPQ Product configurator. We've complex bundle
Sales rep is with customer and sales rep wants to quickly add / remove product from Quote Line Editor (QLE) instead of Product configurator as its time consuming.
Sales rep knows SKU numbers so rep is asking button or something to delete and/or add product in QLE
Appreciate your help.
@gvpatel, from my experience there is not a way to add quotes lines from the QLE outside the configurator.
Options:
- Build a custom configurator (I have not personally done this, but you can find info here: https://www.youtube.com/watch?v=5MVFqoUzZhs and here: https://developer.salesforce.com/docs/atlas.en-us.cpq_dev_plugins.meta/cpq_dev_plugins/cpq_external_config.htm - you might also be able to find some example code on github for the message format. (I don't know if this would be faster or not)
- You could also build something custom outside the QLE just for your use-case where you build a custom UI that is optimized for your use-case and then you can use the CPQ API to add new quote lines and then use normal
delete
DML to remove quote lines. The save operation might still be slow here, so it may not provide you with an optimal solution.
Other than that, nothing comes to mind about a great way to approach this.
Thanks Austin. We also have requirement to create Quote in salesforce from our eCommerce site.
eComm site already make service to call to our legacy system to create Quote but now we're migration to Salesforce CPQ and we need to create quote in Salesforce
Can we use "Save Quote API" service to create quote?
@paustint could you share CPQ_TestUtils class. I have issue with recalculation API. it works for existing products, when I set SeeAllData, but for exactly the same product created in unit test, it doesn't work
Hi @paustint, when I add line item in a quote and execute calculate method its giving me below error
14:35:32:136 FATAL_ERROR System.CalloutException: You have uncommitted work pending. Please commit or rollback before calling out
any idea? I'm using your API only. Appreciate any input.
Thanks,
CPQ_ApiDataModels.QuoteModel quoteModel = CPQ_ApiWrapper.addProductsToQuote('a0q5e000004L3PpAAK', '01t5e000002CkjPAAS', '01s5e00000D18QuAAJ', 'USD', true);
quoteModel.lineItems[1].record.SBQQ__Quantity__c = 2;
CPQ_ApiWrapper.calculateQuote(quoteModel);
CPQ_ApiWrapper.SaveQuote (quoteModel);
It has been a really long time since I have had to work on this, so not exactly sure.
You cannot modify records before the call-out, so you need to make sure nothing in your code is making a database update.
Also if you have any other automation that is re-updating any modified record it could attempt to try to call-out again.
Sorry I cannot be of more help.
Thanks @paustint.
I've inactivated all price rules. Also I'm testing this in my personal dev org so there is no code making update in the database.
If I comment out below line, i could see line items are added and quantity is updated BUT pricing is not calculated.
CPQ_ApiWrapper.calculateQuote(quoteModel);
Hi @paustint,
It seems there are changes in CPQ API methods.
I had to incorporate below changes in CPQ_ApiDataModels.cls and CPQ_ApiWrapper classes.
After incorporating changes I'm no loner getting error "You have uncommitted work pending. Please commit or rollback before calling out"
MyCallback
global with sharing class MyCallback implements SBQQ.CalculateCallback
{
global void callback(String quoteJSON){
SBQQ.ServiceRouter.save('SBQQ.QuoteAPI.QuoteSaver', quoteJSON);
}
}
CPQ_ApiDataModels.cls
public class CalculatorContext {
public QuoteModel quote;
private String callbackClass;
public CalculatorContext(){}
public CalculatorContext(QuoteModel quote, String callbackClass) {
this.quote = quote;
this.callbackClass = callbackClass;
}
}
CPQ_ApiWrapper
public static CPQ_ApiDataModels.QuoteModel calculateQuote(CPQ_ApiDataModels.QuoteModel quoteModel, String callbackClass) {
System.debug(LoggingLevel.WARN, 'CACL-QM: ' + quoteModel);
CPQ_ApiDataModels.CalculatorContext calculatorPayload = new CPQ_ApiDataModels.CalculatorContext(quoteModel, callbackClass);
String updatedQuoteJSON = load(QUOTE_CALCULATOR, null, calculatorPayload);
System.debug(LoggingLevel.WARN, 'CACLupdatedQuoteJSON: ' + updatedQuoteJSON);
CPQ_ApiDataModels.QuoteModel updatedQuoteModel = (CPQ_ApiDataModels.QuoteModel) JSON.deserialize(updatedQuoteJSON, CPQ_ApiDataModels.QuoteModel.class);
if(debug) {
System.debug(LoggingLevel.WARN, 'updatedQuoteJSON: ' + updatedQuoteJSON);
System.debug(LoggingLevel.WARN, 'updatedQuoteModel: ' + updatedQuoteModel);
}
return updatedQuoteModel;
}
CPQ_ApiDataModels.QuoteModel quoteModel = CPQ_ApiWrapper.addProductsToQuote('a0q5e000004L3PpAAK', '01t5e000002CkjPAAS', '01s5e00000D18QuAAJ', 'USD', true);
quoteModel.record.SBQQ__TargetCustomerAmount__c = 40;
quoteModel.lineItems[1].record.SBQQ__Quantity__c = 2;
quoteModel.applyAdditionalDiscountLast = true;
CPQ_ApiWrapper.calculateQuote(quoteModel, 'MyCallback');
For the benefit of other readers I've added new overloaded method addProductsToQuote. Here is what it does
- Add standalone products to the quote
- Add bundle product and automatically select options that are part of bundle
public static CPQ_ApiDataModels.QuoteModel addProductsToQuote(Id quoteId, Id pricebookId, String currencyCode,
List < String > lstOfStandAloneProd, Id BundleId, List < String > lstOfBundleChildren, Boolean skipCalculate){
//Query Quote and Load QuoteModel
CPQ_ApiDataModels.QuoteModel QuoteModel = getQuoteModel(quoteId);
List < CPQ_ApiDataModels.ProductModel > listOfProductModel = new List<CPQ_ApiDataModels.ProductModel>();
for (Integer i = 0; i < lstOfStandAloneProd.size(); i++) {
//This loop will print all the elements in array
system.debug('Values In Array: ' + lstOfStandAloneProd[i]);
CPQ_ApiDataModels.ProductModel productModel = loadProduct(lstOfStandAloneProd[i], pricebookId, currencyCode);
listOfProductModel.add(productModel);
}
if (BundleId != null) {
CPQ_ApiDataModels.ProductModel productBundleModel = loadProduct(BundleId, pricebookId, currencyCode);
List < CPQ_ApiDataModels.ConfigurationModel > lstConfigurationModal = new List<CPQ_ApiDataModels.ConfigurationModel>();
SBQQ__ProductOption__c productOption = new SBQQ__ProductOption__c();
for (CPQ_ApiDataModels.OptionModel opModel : productBundleModel.options) {
if (lstOfBundleChildren.contains(opModel.record.SBQQ__OptionalSKU__c)) {
system.debug(
'productBundleModel.options: ' + productBundleModel.options
);
CPQ_ApiDataModels.ConfigurationModel cfModel = new CPQ_ApiDataModels.ConfigurationModel();
system.debug('Option Data');
system.debug(opModel.record);
cfModel.configuredProductId = opModel.record.SBQQ__OptionalSKU__c;
cfModel.optionId = opModel.record.Id;
cfModel.optionData = opModel.record;
productOption = opModel.record;
cfModel.validationMessages = new List<String>();
cfModel.priceEditable = false;
cfModel.configured = false;
cfModel.optionConfigurations = new List<CPQ_ApiDataModels.ConfigurationModel>();
cfModel.listPrice = null;
cfModel.isUpgrade = false;
cfModel.isDynamicOption = false;
cfModel.inheritedConfigurationData = null;
cfModel.hiddenOptionIds = new Set<Id>();
cfModel.dynamicOptionKey = null;
cfModel.disabledOptionIds = new Set<Id>();
cfModel.configurationData = new SBQQ__ProductOption__c();
cfModel.changedByProductActions = false;
lstConfigurationModal.add(cfModel);
}
}
if (productBundleModel.configuration != null) {
if (productBundleModel.configuration.optionConfigurations != null) {
productBundleModel.configuration.optionConfigurations.addAll(lstConfigurationModal);
}
productBundleModel.configuration.configured = true;
listOfProductModel.add(productBundleModel);
}
}
CPQ_ApiDataModels.ProductAddContext productAddPayload = new CPQ_ApiDataModels.ProductAddContext(skipCalculate, QuoteModel, listOfProductModel);
CPQ_ApiDataModels.QuoteModel QM = addProductsToQuote(productAddPayload);
return QM; }
String QuoteId = 'a0q5e000004L58rAAC';
String BundleId = '01t5e000002wqhlAAA';
String[] lstOfBundleChildren = new String[]{'01t5e000000URKpAAO', '01t5e000000URKqAAO'};
String[] lstOfStandAloneProd = new String[]{'01t5e000000URM7AAO', '01t5e000000URKeAAO'};
String PrieBookId='01s5e00000D18QuAAJ';
String Curcy ='USD';
CPQ_ApiDataModels.QuoteModel quoteModel = CPQ_ApiWrapper.addProductsToQuote(QuoteId,PrieBookId, Curcy,lstOfStandAloneProd, BundleId, lstOfBundleChildren, true);
CPQ_ApiWrapper.calculateQuote(quoteModel, 'MyCallback');
Hi, I am able to succesfully create new quoteline with this code but the new line is only visible when I refresh the quote line editor. The idea is to automatically add a product(Maintenance) based on the other products in the quote. I could'nt use product rule because I need this product to be added several times in the same quote which is not possible with product rules.
Could you please help? Thanks in advance!
//Get Quote as JSON formatted String
String QuoteId = 'aGaJX000000oFyL0AU';
String productId = '01t200000057ROwAAM';
String pricebookId = '01s3X000007Fj4uQAC';
String currencycode = 'EUR';
String quoteJSON = SBQQ.ServiceRouter.read('SBQQ.QuoteAPI.QuoteReader', QuoteId);
system.debug('QuoteMOdel' +quoteJSON);
//Get Product info as JSON formatted String representing the product to be added to the quote
String productModel = SBQQ.ServiceRouter.load('SBQQ.ProductAPI.ProductLoader',
productId,
'{"pricebookId" : "' + priceBookId + '", "currencyCode" : "' + currencyCode + '"}');
system.debug('ProductModel'+productModel);
//Now, add the product added to to the Quote
String updatedQuoteModel = SBQQ.ServiceRouter.load('SBQQ.QuoteAPI.QuoteProductAdder', null,
'{"quote" : ' + quoteJSON + ',"products" : [' + productModel + '],"ignoreCalculate" : true}');
system.debug('Quoteline is inserted');
System.debug('Updated QuoteModel'+updatedQuoteModel);
//Save quote and lines
String savedQuoteModel = SBQQ.ServiceRouter.save('SBQQ.QuoteAPI.QuoteSaver', updatedQuoteModel);
system.debug('Saved Quote Model' +savedQuoteModel);
Hello @gvpatel,
thank you for your sample script to add a bundle product, I'd have a request: would you also have a sample script to modify the configuration (i.e. add/remove an option) of an existing quote item?
Thanks in advace for your attention!
@paustint Need your expertise. I'm running in to a null pointer exception while trying to load Product using CPQ_ApiDataModels.ProductModel loadProduct(Id productId, Id pricebookId, String currencyCode).
SBQQ.ServiceRouter.load('SBQQ.ProductAPI.ProductLoader', productId, '{"pricebookId" : "' + pricebookId + '", "currencyCode" : "' + currencyCode + '"}');