Dynamic Pricing Implementation: Price Rules and QCP in Salesforce CPQ
This guide gives developers and solution architects a blueprint to accelerate development cycles and enhance the implementation of nuanced pricing strategies.
Join the DZone community and get the full member experience.
Join For FreeIn today's rapidly evolving go-to-market landscape, organizations with diverse product portfolios face intricate pricing and discounting challenges. The implementation of a robust, scalable pricing framework has become paramount to maintaining competitive edge and operational efficiency. This study delves into the strategic utilization of Salesforce CPQ's advanced features, specifically price rules and Quote Calculator Plugins (QCP), to address complex dynamic pricing scenarios.
This guide presents an in-depth analysis of ten sophisticated use cases, demonstrating how these automation tools can be harnessed to create agile, responsive pricing models. By emphasizing low-code and declarative configuration methodology, this comprehensive guide provides software developers and solution architects with a blueprint to accelerate development cycles and enhance the implementation of nuanced pricing strategies.
What Are Price Rules and QCP?
Price Rules in Salesforce CPQ
Price Rules are a feature in Salesforce CPQ that allows users to define automated pricing logic. They apply discounts, adjust prices, or add charges based on specified conditions, enabling complex pricing scenarios without custom code. To implement these complex rules in Salesforce CPQ, you'll often need to combine multiple features such as Price Rules, Price Conditions, Price Actions, Custom Fields, Formula Fields, Product Rules, and Lookup Query objects. Adequately set Evaluation event (Before/On/After calculation) and the evaluation order of price rules to avoid any row-lock or incorrect updates.
QCP (Quote Calculator Plugin)
QCP is a JavaScript-based customization tool in Salesforce CPQ that allows for advanced, custom pricing calculations. It provides programmatic access to the quote model, enabling complex pricing logic beyond standard CPQ features. First, you'll need to enable the QCP in your Salesforce CPQ settings. Then, you can create a new QCP script or modify an existing one. When needed, make sure QCP has access to the quote, line items, and other CPQ objects.
QCP has a character limit; therefore, it is advised that it should only be used for logic which cannot be implemented with any declarative CPQ method. Additionally, you may need to use Apex code for more complex calculations or integrations with external systems.
Use Case Examples Using Price Rules and QCP
Use Case 1: Volume-Based Tiered Discounting
Apply different discount percentages based on quantity ranges. For example:
Label
|
Minimum_Quantity__c
|
Maximum_Quantity__c
|
Discount_Percentage__c
|
---|---|---|---|
Tier 1
|
1
|
10
|
0
|
Tier 2
|
11
|
50
|
5
|
Tier 3
|
51
|
100
|
10
|
Tier 4
|
101
|
999999
|
15
|
Price Rule Implementation
Use Price Rules with Lookup Query objects to define tiers and corresponding discounts.
- Create New Price Rule:
- Name: Volume-Based Tiered Discount
- Active: True
- Evaluation Event: On Calculate
- Calculator: Default Calculator
- Conditions Met: All
- Add Lookup Query to Price Rule:
- Name: Volume Discount Tier Lookup
- Lookup Object: Volume Discount Tier (the above table represents this Lookup Object)
- Match Type: Single
- Input Field: Quantity
- Operator: Between
- Low-Value Field: Minimum_Quantity__c
- High-Value Field: Maximum_Quantity__c
- Return Field: Discount_Percentage__c
- Add Price Action to Price Rule:
- Type: Discount (Percent)
- Value Source: Lookup
- Lookup Object: Volume Discount Tier Lookup
- Source Variable: Return Value
- Target Object: Line
- Target Field: Discount
With this configuration, any number of discount tiers could be supported as per the volume being ordered. Lookup tables/objects provide a great way to handle a dynamic pricing framework.
QCP Implementation
Now, let's see how the same use case can be implemented with the QCP script. The code can be invoked with Before/On/After calculating events as per the need of the use case.
function applyVolumeTieredDiscount(lineItems) {
lineItems.forEach(item => {
let discount = 0;
if (item.Quantity > 100) {
discount = 15;
} else if (item.Quantity > 50) {
discount = 10;
} else if (item.Quantity > 10) {
discount = 5;
}
item.Discount = discount;
});
}
Use Case 2: Bundle Pricing
Offer special pricing when specific products are purchased together. For instance, a computer, monitor, and keyboard might have a lower total price when bought as a bundle vs individual components.
Price Rule Implementation
Create Product Bundles and use Price Rules to apply discounts when all components are present in the quote.
- Create a new Price Rule:
- Name: Bundle Discount
- Active: True
- Evaluation Event: On Calculate
- Calculator: Default Calculator
- Conditions Met: All
- Add Price Conditions:
- Condition 1:
- Field: Product Code
- Operator: Equals
- Filter Value: PROD-A
- Condition 2:
- Field: Quote.Line Items.Product Code
- Operator: Contains
- Filter Value: PROD-B
- Condition 3:
- Field: Quote.Line Items.Product Code
- Operator: Contains
- Filter Value: PROD-C
- Condition 1:
- Add Price Action:
- Type: Discount (Absolute)
- Value: 100 // $100 discount for the bundle
- Apply To: Group
- Apply Immediately: True
QCP Implementation
function applyBundlePricing(lineItems) {
const bundleComponents = ['Product A', 'Product B', 'Product C'];
const allComponentsPresent = bundleComponents.every(component =>
lineItems.some(item => item.Product.Name === component)
);
if (allComponentsPresent) {
const bundleDiscount = 100; // $100 discount for the bundle
lineItems.forEach(item => {
if (bundleComponents.includes(item.Product.Name)) {
item.Additional_Discount__c = bundleDiscount / bundleComponents.length;
}
});
}
}
Use Case 3: Cross-Product Conditional Discounting
Apply discounts on one product based on the purchase of another. For example, offer a 20% discount on software licenses if the customer buys a specific hardware product.
Price Rule Implementation
Use Price Conditions to check for the presence of the conditional product and Price Actions to apply the discount on the target product.
- Create a new Price Rule:
- Name: Product Y Discount
- Active: True
- Evaluation Event: On Calculate
- Calculator: Default Calculator
- Conditions Met: All
- Add Price Conditions:
- Condition 1:
- Field: Product Code
- Operator: Equals
- Filter Value: PROD-Y
- Condition 2:
- Field: Quote.Line Items.Product Code
- Operator: Contains
- Filter Value: PROD-X
- Condition 1:
- Add Price Action:
- Type: Discount (Percent)
- Value: 20
- Apply To: Line
- Apply Immediately: True
QCP Implementation
function applyCrossProductDiscount(lineItems) {
const hasProductX = lineItems.some(item => item.Product.Name === 'Product X');
if (hasProductX) {
lineItems.forEach(item => {
if (item.Product.Name === 'Product Y') {
item.Discount = 20;
}
});
}
}
Use Case 4: Time-Based Pricing
Adjust prices based on subscription length or contract duration. For instance, offer a 10% discount for 2-year contracts and 15% for 3-year contracts.
Price Rule Implementation
Use Quote Term fields and Price Rules to apply discounts based on the contract duration. This use case demonstrates the use of another important feature, the Price Action Formula.
- Create a new Price Rule:
- Name: Contract Duration Discount
- Active: True
- Evaluation Event: On Calculate
- Calculator: Default Calculator
- Conditions Met: All
- Add Price Condition: (to avoid invocation of price action for every calculation)
- Type: Custom
- Advanced Condition: Quote.Subscription_Term__c >= 24
- Add Price Action:
- Type: Discount (Percent)
- Value Source: Formula
- Apply To: Line
- Apply Immediately: True
- Formula:
CASE(
FLOOR(Quote.Subscription_Term__c / 12),
2, 10,
3, 15,
4, 20,
5, 25,
0
)
This approach offers several advantages:
- It combines multiple tiers into a single price rule, making it easier to manage.
- It's more flexible and can easily accommodate additional tiers by adding more cases to the formula.
- It uses a formula-based approach, which can be modified without needing to create multiple price rules for each tier.
QCP Implementation
function applyTimeBasedPricing(quote, lineItems) {
const contractDuration = quote.Contract_Duration_Months__c;
let discount = 0;
if (contractDuration >= 36) {
discount = 15;
} else if (contractDuration >= 24) {
discount = 10;
}
lineItems.forEach(item => {
item.Additional_Discount__c = discount;
});
}
Use Case 5: Customer/Market Segment-Specific Pricing
Set different prices for various customer categories. For example, enterprise customers might get a 25% discount, while SMBs get a 10% discount.
Price Rule Implementation
Use Account fields to categorize customers and Price Rules to apply segment-specific discounts.
- Create a new Price Rule:
- Name: Customer Segment Discount
- Active: True
- Evaluation Event: On Calculate
- Calculator: Default Calculator
- Conditions Met: All
- Add Price Condition:
- Type: Custom
- Advanced Condition: Quote.Account.Customer_Segment__c is not blank
- Add Price Action:
- Type: Discount (Percent)
- Value Source: Formula
- Apply To: Line
- Apply Immediately: True
- Formula:
CASE(
Quote.Account.Customer_Segment__c,
'Enterprise', 25,
'Strategic', 30,
'SMB', 10,
'Startup', 5,
'Government', 15,
0
)
QCP Implementation
function applyCustomerSegmentPricing(quote, lineItems) {
const customerSegment = quote.Account.Customer_Segment__c;
let discount = 0;
switch (customerSegment) {
case 'Enterprise':
discount = 25;
break;
case 'SMB':
discount = 10;
break;
}
lineItems.forEach(item => {
item.Additional_Discount__c = discount;
});
}
Use Case 6: Competitive Pricing Rules
Automatically adjust prices based on competitors' pricing data. For instance, always price your product 5% below a specific competitor's price.
Price Rule Implementation
Create custom fields to store competitor pricing data on the product object and use Price Rules with formula fields to calculate and apply the adjusted price.
- Create a new Price Rule:
- Name: Competitive Pricing
- Active: True
- Evaluation Event: On Calculate
- Calculator: Default Calculator
- Conditions Met: All
- Add Price Condition:
- Field: Competitor_Price__c
- Operator: Is Not Null
- Add Price Actions:
- Action 1:
- Type: Custom
- Value Field: Competitor_Price__c * 0.95
- Target Field: Special_Price__c
- Action 2 (to ensure price doesn't go below floor price):
- Type: Price
- Value Source: Formula
- Formula: MAX(Special_Price__c, Floor_Price__c)
- Target Field: Special_Price__c
- Action 1:
QCP Implementation
function applyCompetitivePricing(lineItems) {
lineItems.forEach(item => {
if (item.Competitor_Price__c) {
const ourPrice = item.Competitor_Price__c * 0.95; // 5% below competitor
const minimumPrice = item.Floor_Price__c || item.ListPrice * 0.8; // 20% below list price as floor
item.Special_Price__c = Math.max(ourPrice, minimumPrice);
}
});
}
Use Case 7: Multi-Currency Pricing
Apply different pricing rules based on the currency used in the transaction. For example, offer a 5% discount for USD transactions but a 3% discount for EUR transactions. The discounted prices can be maintained directly in the Pricebook entry of a particular product however, the price rules can extend the conditional logic further to add a dynamic pricing element based on various conditions based on quote and quote line-specific data.
Price Rule Implementation
Use the Multi-Currency feature in Salesforce and create Price Rules that consider the Quote Currency field. The lookup table approach will provide further flexibility to the approach.
Label
|
Currency_Code__c
|
Discount_Percentage__c
|
---|---|---|
USD
|
USD
|
5
|
EUR
|
EUR
|
3
|
GBP
|
GBP
|
4
|
JPY
|
JPY
|
2
|
CAD
|
CAD
|
4.5
|
AUD
|
AUD
|
3.5
|
CHF
|
CHF
|
2.5
|
- Create Price Rule
- Name: Multi-Currency Discount
- Active: True
- Evaluation Event: On Calculate
- Calculator: Default Calculator
- Conditions Met: All
- Add Lookup Query to Price Rule (above table represents the structure of Currency Discount object)
- Name: Currency Discount Lookup
- Lookup Object: Currency Discount
- Match Type: Single
- Input Field: CurrencyIsoCode
- Operator: Equals
- Comparison Field: Currency_Code__c
- Return Field: Discount_Percentage__c
- Add Price Action to Price Rule
- Type: Discount (Percent)
- Value Source: Lookup
- Lookup Object: Currency Discount Lookup
- Source Variable: Return Value
- Target Object: Line
- Target Field: Discount
QCP Implementation
function applyMultiCurrencyPricing(quote, lineItems) {
const currency = quote.CurrencyIsoCode;
let discount = 0;
switch (currency) {
case 'USD':
discount = 5;
break;
case 'EUR':
discount = 3;
break;
} //add more currencies as needed
lineItems.forEach(item => {
item.Additional_Discount__c = discount;
});
}
Use Case 8: Margin-Based Pricing
Dynamically adjust prices to maintain a specific profit margin. For instance, ensure a minimum 20% margin on all products.
Price Rule Implementation
Create custom fields for cost data and use Price Rules with formula fields to calculate and enforce minimum prices based on desired margins.
- Create a new Price Rule:
- Name: Minimum Margin
- Active: True
- Evaluation Event: On Calculate
- Calculator: Default Calculator
- Conditions Met: All
- Add Price Condition:
- Field: (List Price - Cost__c) / List Price
- Operator: Less Than
- Filter Value: 0.20
- Add Price Action:
- Type: Custom
- Value Field: Cost__c / (1 - 0.20)
- Target Field: Special_Price__c
QCP Implementation
function applyMarginBasedPricing(lineItems) {
const desiredMargin = 0.20; // 20% margin
lineItems.forEach(item => {
if (item.Cost__c) {
const minimumPrice = item.Cost__c / (1 - desiredMargin);
if (item.NetPrice < minimumPrice) {
item.Special_Price__c = minimumPrice;
}
}
});
}
Use Case 9: Geolocation-Based Pricing
Set different prices based on the customer's geographical location. Geolocation-based pricing with multiple levels. Apply different pricing adjustments based on the following hierarchy.
Price Rule Implementation
Use Account, User, or Quote fields to store location data and create Price Rules that apply location-specific adjustments.
Label
|
Sales_Region__c
|
Area__c
|
Sub_Area__c
|
Price_Adjustment__c
|
---|---|---|---|---|
NA_USA_CA
|
North America
|
USA
|
California
|
1.1
|
NA_USA_NY
|
North America
|
USA
|
New York
|
1.15
|
NA_Canada
|
North America
|
Canada
|
null
|
1.05
|
EU_UK_London
|
Europe
|
UK
|
London
|
1.2
|
EU_Germany
|
Europe
|
Germany
|
null
|
1.08
|
APAC_Japan
|
Asia-Pacific
|
Japan
|
null
|
1.12
|
- Create the Price Rule:
- Name: Geolocation Based Pricing
- Active: True
- Evaluation Event: On Calculate
- Calculator: Default Calculator
- Conditions Met: All
- Add Lookup Query to Price Rule
- Name: Geo Pricing Lookup
- Lookup Object: Geo Pricing
- Match Type: Single
- Input Field 1: Quote.Account.Sales_Region__c
- Operator: Equals
- Comparison Field: Sales_Region__c
- Input Field 2: Quote.Account.BillingCountry
- Operator: Equals
- Comparison Field: Area__c
- Input Field 3: Quote.Account.BillingState
- Operator: Equals
- Comparison Field: Sub_Area__c
- Return Field: Price_Adjustment__c
- Add Price Action to Price Rule
- Type: Percent Of List
- Value Source: Lookup
- Lookup Object: Geo Pricing Lookup
- Source Variable: Return Value
- Target Object: Line
- Target Field: Special Price
QCP Implementation
export function onBeforeCalculate(quote, lines, conn) {
applyGeoPricing(quote, lines);
}
function applyGeoPricing(quote, lines) {
const account = quote.record.Account;
const salesRegion = account.Sales_Region__c;
const area = account.BillingCountry;
const subArea = account.BillingState;
// Fetch the geo pricing adjustment
const geoPricing = getGeoPricing(salesRegion, area, subArea);
if (geoPricing) {
lines.forEach(line => {
line.record.Special_Price__c = line.record.ListPrice * geoPricing.Price_Adjustment__c;
});
}
}
function getGeoPricing(salesRegion, area, subArea) {
// This is a simplified version. In a real scenario, you'd query the Custom Metadata Type.
// For demonstration, we're using a hardcoded object.
const geoPricings = [
{ Sales_Region__c: 'North America', Area__c: 'USA', Sub_Area__c: 'California', Price_Adjustment__c: 1.10 },
{ Sales_Region__c: 'North America', Area__c: 'USA', Sub_Area__c: 'New York', Price_Adjustment__c: 1.15 },
{ Sales_Region__c: 'North America', Area__c: 'Canada', Sub_Area__c: null, Price_Adjustment__c: 1.05 },
{ Sales_Region__c: 'Europe', Area__c: 'UK', Sub_Area__c: 'London', Price_Adjustment__c: 1.20 },
{ Sales_Region__c: 'Europe', Area__c: 'Germany', Sub_Area__c: null, Price_Adjustment__c: 1.08 },
{ Sales_Region__c: 'Asia-Pacific', Area__c: 'Japan', Sub_Area__c: null, Price_Adjustment__c: 1.12 }
];
// Find the most specific match
return geoPricings.find(gp =>
gp.Sales_Region__c === salesRegion &&
gp.Area__c === area &&
gp.Sub_Area__c === subArea
) || geoPricings.find(gp =>
gp.Sales_Region__c === salesRegion &&
gp.Area__c === area &&
gp.Sub_Area__c === null
) || geoPricings.find(gp =>
gp.Sales_Region__c === salesRegion &&
gp.Area__c === null &&
gp.Sub_Area__c === null
);
}
Use Case 10: Usage-Based Pricing
Implement complex calculations for pricing based on estimated or actual usage. For instance, price cloud storage based on projected data volume and access frequency.
Price Rule Implementation
A tiered pricing model for a cloud storage service based on the estimated monthly usage. The pricing will have a base price and additional charges for usage tiers. This implementation has another variety approach of leveraging custom metadata and configuration settings along with native price rule functionalities.
Pricing Model:
- Base Price: $100 per month
- 0-1000 GB: Included in base price
- 1001-5000 GB: $0.05 per GB
- 5001-10000 GB: $0.04 per GB
- 10001+ GB: $0.03 per GB
Step 1: Create Custom Metadata Type in Salesforce setup:
- Go to Setup > Custom Metadata Types
- Click "New Custom Metadata Type"
- Label: Usage Pricing Tier
- Plural Label: Usage Pricing Tiers
- Object Name: Usage_Pricing_Tier__mdt
- Add custom fields:
- Minimum_Usage__c (Number)
- Maximum_Usage__c (Number)
- Price_Per_GB__c (Currency)
Step 2: Add records to the Custom Metadata Type:
Label
|
Minimum_Usage__c
|
Maximum_Usage__c
|
Price_Per_GB__c
|
---|---|---|---|
Tier 1
|
0
|
1000
|
0
|
Tier 2
|
1001
|
5000
|
0.05
|
Tier 3
|
5001
|
10000
|
0.04
|
Tier 4
|
10001
|
999999999
|
0.03
|
- Create the Price Rule:
- Name: Usage-Based Pricing
- Active: True
- Evaluation Event: On Calculate
- Calculator: Default Calculator
- Conditions Met: All
- Add Price Condition
- Field: Product.Pricing_Model__c
- Operator: Equals
- Filter Value: Usage-Based
- Add Lookup Query to Price Rule
- Name: Usage Pricing Tier Lookup
- Lookup Object: Usage Pricing Tier
- Match Type: Single
- Input Field: Estimated_Monthly_Usage__c
- Operator: Between
- Low-Value Field: Minimum_Usage__c
- High-Value Field: Maximum_Usage__c
- Return Field: Price_Per_GB__c
- Add Price Action to Price Rule
- Type: Custom
- Value Source: Formula
- Target Object: Line
- Target Field: Special_Price__c
- Formula:
100 + (MAX(Estimated_Monthly_Usage__c - 1000, 0) *
Usage_Pricing_Tier_Lookup.Price_Per_GB__c)
QCP Implementation
export function onBeforeCalculate(quote, lines, conn) {
applyUsageBasedPricing(quote, lines);
}
function applyUsageBasedPricing(quote, lines) {
lines.forEach(line => {
if (line.record.Product__r.Pricing_Model__c === 'Usage-Based') {
const usage = line.record.Estimated_Monthly_Usage__c || 0;
const basePrice = 100;
let additionalCost = 0;
if (usage > 1000) {
additionalCost += calculateTierCost(usage, 1001, 5000, 0.05);
}
if (usage > 5000) {
additionalCost += calculateTierCost(usage, 5001, 10000, 0.04);
}
if (usage > 10000) {
additionalCost += calculateTierCost(usage, 10001, usage, 0.03);
}
line.record.Special_Price__c = basePrice + additionalCost;
}
});
}
function calculateTierCost(usage, tierStart, tierEnd, pricePerGB) {
const usageInTier = Math.min(usage, tierEnd) - tierStart + 1;
return Math.max(usageInTier, 0) * pricePerGB;
}
// Optional: Add a function to provide usage tier information to the user
export function onAfterCalculate(quote, lines, conn) {
lines.forEach(line => {
if (line.record.Product__r.Pricing_Model__c === 'Usage-Based') {
const usage = line.record.Estimated_Monthly_Usage__c || 0;
const tierInfo = getUsageTierInfo(usage);
line.record.Usage_Tier_Info__c = tierInfo;
}
});
}
function getUsageTierInfo(usage) {
if (usage <= 1000) {
return 'Tier 1: 0-1000 GB (Included in base price)';
} else if (usage <= 5000) {
return 'Tier 2: 1001-5000 GB ($0.05 per GB)';
} else if (usage <= 10000) {
return 'Tier 3: 5001-10000 GB ($0.04 per GB)';
} else {
return 'Tier 4: 10001+ GB ($0.03 per GB)';
}
}
Likewise, there are a plethora of use cases that can be implemented using price rule configuration. The recommendation is to always use a declarative approach before turning to QCP, which is specifically available as an extension to the price rule engine.
- Note: The rules and scripts above are not compiled. They are added as a demonstration for explanation purposes.
Conclusion
Salesforce CPQ's Price Rules and Quote Calculator Plugin (QCP) offer a powerful combination for implementing dynamic pricing strategies. Price Rules provide a declarative approach for straightforward pricing logic, while QCP enables complex, programmatic calculations. When used with Custom Metadata Types/Custom lookup objects, these tools create a flexible, scalable, and easily maintainable pricing system. Together, they can address a wide range of pricing needs, from simple to highly sophisticated, allowing businesses to adapt quickly to market changes and implement nuanced pricing strategies. This versatility enables organizations to optimize sales processes, improve profit margins, and respond effectively to diverse customer needs within the Salesforce CPQ ecosystem.
Opinions expressed by DZone contributors are their own.
Comments