- 
      
- 
        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