-
-
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()); | |
//} | |
} |
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 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