Monday, March 4, 2024

Salesforce Apex: Decorator Pattern

The Decorator design pattern is a structural design pattern used to dynamically add behavior to an object without altering its structure. In Salesforce Apex, this can be particularly useful when you want to enhance the functionality of an object without changing its existing code, which is a common scenario when dealing with managed packages like Salesforce CPQ (formerly Steelbrick CPQ), where you cannot modify the source code.

A common functionality in Salesforce CPQ is the pricing calculation of QuoteLineItems, where you might need to apply different pricing strategies based on certain criteria. The Decorator pattern allows you to dynamically adjust the pricing calculations without modifying the original QuoteLineItem code.

Here's a simplified example demonstrating how we could use the Decorator pattern in Salesforce Apex to apply different pricing strategies to CPQ QuoteLineItems:

First, let's create an interface that will define the common functionality for our Quote line items:

public interface IQuoteLineItem {
    void calculatePrice();
    Decimal getPrice();
}

Next, we have a basic implementation of our Quote line item:

public class BasicQuoteLineItem implements IQuoteLineItem {
    private Decimal basePrice;
    
    public BasicQuoteLineItem(Decimal price) {
        this.basePrice = price;
    }
    
    public void calculatePrice() {
        // Basic price calculation logic
    }
    
    public Decimal getPrice() {
        return basePrice;
    }
}

Now, we can create our decorators to extend the functionality. Each decorator implements the same interface and takes in an instance of IQuoteLineItem.

public abstract class QuoteLineItemDecorator implements IQuoteLineItem {
    protected IQuoteLineItem decoratedItem;
    
    public QuoteLineItemDecorator(IQuoteLineItem item) {
        this.decoratedItem = item;
    }
    
    public void calculatePrice() {
        decoratedItem.calculatePrice();
    }
    
    public Decimal getPrice() {
        return decoratedItem.getPrice();
    }
}

public class DiscountDecorator extends QuoteLineItemDecorator {
    private Decimal discountRate;
    
    public DiscountDecorator(IQuoteLineItem item, Decimal discountRate) {
        super(item);
        this.discountRate = discountRate;
    }
    
    public void calculatePrice() {
        super.calculatePrice();
        applyDiscount();
    }
    
    private void applyDiscount() {
        Decimal price = decoratedItem.getPrice();
        decoratedItem.getPrice() * (1 - discountRate);
    }
}

public class TaxDecorator extends QuoteLineItemDecorator {
    private Decimal taxRate;
    
    public TaxDecorator(IQuoteLineItem item, Decimal taxRate) {
        super(item);
        this.taxRate = taxRate;
    }
    
    public void calculatePrice() {
        super.calculatePrice();
        addTax();
    }
    
    private void addTax() {
        Decimal price = decoratedItem.getPrice();
        decoratedItem.getPrice() * (1 + taxRate);
    }
}

Finally, we can use these decorators to dynamically add behavior to our QuoteLineItem objects:

List<IQuoteLineItem> quoteLineItems = new List<IQuoteLineItem>();

for (QuoteLineItem qli : [SELECT Id, UnitPrice FROM QuoteLineItem WHERE QuoteId = :someQuoteId]) {
    IQuoteLineItem item = new BasicQuoteLineItem(qli.UnitPrice);
    
    // Apply discounts only if the item qualifies
    if (meetsDiscountCriteria(qli)) {
        item = new DiscountDecorator(item, 0.10); // 10% discount
    }
    
    // Apply tax to all items
    item = new TaxDecorator(item, 0.08); // 8% tax
    
    item.calculatePrice(); // Calculate final price
    quoteLineItems.add(item);
}

In the example above, we query all the QuoteLineItem records outside of the loop and then process them in bulk. However, you would need to add additional logic to handle updates to these records back to the database, ensuring that you collect all the updated items and perform a single DML update outside of the loop.

2nd Example

One common requirement might be to apply additional discounts or fees based on complex business rules that are not covered by standard CPQ functionality. For instance, you might need to apply volume discounts, promotional discounts, or special fees that depend on the combination of products and customer attributes.

Here's an example of how you could use the Decorator pattern to add custom discounting logic to CPQ quote lines in a bulkified way:

Step 1: Define the interface

public interface IQuoteLineCalculator {
    void calculate(List<SBQQ__QuoteLine__c> quoteLines);
}

Step 2: Implement the base calculator

public class BaseQuoteLineCalculator implements IQuoteLineCalculator {
    public void calculate(List<SBQQ__QuoteLine__c> quoteLines) {
        // Here you would apply the base CPQ pricing logic
        // For simplicity, let's assume this is a no-op, since CPQ calculates base prices
    }
}

Step 3: Implement the decorators

public abstract class QuoteLineCalculatorDecorator implements IQuoteLineCalculator {
    protected IQuoteLineCalculator innerCalculator;
    
    public QuoteLineCalculatorDecorator(IQuoteLineCalculator inner) {
        this.innerCalculator = inner;
    }
    
    public void calculate(List<SBQQ__QuoteLine__c> quoteLines) {
        // Delegate to the wrapped calculator
        innerCalculator.calculate(quoteLines);
    }
}

public class VolumeDiscountDecorator extends QuoteLineCalculatorDecorator {
    public VolumeDiscountDecorator(IQuoteLineCalculator inner) {
        super(inner);
    }
    
    public override void calculate(List<SBQQ__QuoteLine__c> quoteLines) {
        // First, let the inner calculator do its work
        super.calculate(quoteLines);
        
        // Then apply volume discounts
        for (SBQQ__QuoteLine__c line : quoteLines) {
            Integer quantity = line.SBQQ__Quantity__c;
            // Example volume discount logic
            if (quantity > 100) {
                line.SBQQ__AdditionalDiscount__c = 0.1; // 10% discount
            }
        }
    }
}

public class PromotionalDiscountDecorator extends QuoteLineCalculatorDecorator {
    public PromotionalDiscountDecorator(IQuoteLineCalculator inner) {
        super(inner);
    }
    
    public override void calculate(List<SBQQ__QuoteLine__c> quoteLines) {
        // Apply the inner calculator's logic
        super.calculate(quoteLines);
        
        // Then apply promotional discounts
        for (SBQQ__QuoteLine__c line : quoteLines) {
            // Example promotional discount logic
            if (isPromotionalProduct(line.SBQQ__Product__c)) {
                line.SBQQ__AdditionalDiscount__c += 0.05; // Additional 5% discount
            }
        }
    }
    
    private Boolean isPromotionalProduct(Id productId) {
        // Implement logic to determine if the product is on promotion
        return true; // Simplified for example purposes
    }
}

Step 4: Use the decorators in your CPQ plugin or custom logic

public with sharing class CustomQuoteCalculator {

    public void calculateQuoteLineAdjustments(List<SBQQ__QuoteLine__c> quoteLines) {
        IQuoteLineCalculator calculator = new BaseQuoteLineCalculator();
        
        // Wrap the calculator with decorators as needed
        calculator = new VolumeDiscountDecorator(calculator);
        calculator = new PromotionalDiscountDecorator(calculator);
        
        // Perform calculations
        calculator.calculate(quoteLines);
        
        // Update the quote lines in bulk
        update quoteLines;
    }

}

This example demonstrates how the Decorator pattern can be applied in Salesforce CPQ to compose different pricing behaviors dynamically. Each decorator adds its layer of logic on top of the existing calculations. The BaseQuoteLineCalculator represents the standard CPQ pricing logic, and each decorator adds additional adjustments like volume discounts or promotional discounts.

The CustomQuoteCalculator class demonstrates how these decorators can be used in a CPQ plugin or custom logic, ensuring that all calculations are done in bulk to avoid governor limits. The list of SBQQ__QuoteLine__c records is passed through each decorator, with each decorator applying its specific adjustments.

This pattern offers a clean and maintainable structure for extending CPQ logic, enabling you to add or remove decorators as business requirements evolve without modifying the underlying classes.

Share This:    Facebook Twitter

0 comments:

Post a Comment

Total Pageviews

My Social Profiles

View Sonal's profile on LinkedIn

Tags

__proto__ $Browser Access Grants Accessor properties Admin Ajax AllowsCallouts Apex Apex Map Apex Sharing AssignmentRuleHeader AsyncApexJob Asynchronous Auth Provider AWS Callbacks Connected app constructor Cookie CPU Time CSP Trusted Sites CSS Custom settings CustomLabels Data properties Database.Batchable Database.BatchableContext Database.query Describe Result Destructuring Dynamic Apex Dynamic SOQL Einstein Analytics enqueueJob Enterprise Territory Management Enumeration escapeSingleQuotes featured Flows geolocation getGlobalDescribe getOrgDefaults() getPicklistValues getRecordTypeId() getRecordTypeInfosByName() getURLParameters Google Maps Governor Limits hasOwnProperty() Heap Heap Size IIFE Immediately Invoked Function Expression Interview questions isCustom() Javascript Javascript Array jsForce Lightning Lightning Components Lightning Events lightning-record-edit-form lightning:combobox lightning:icon lightning:input lightning:select LockerService Lookup LWC Manual Sharing Map Modal Module Pattern Named Credentials NodeJS OAuth Object.freeze() Object.keys() Object.preventExtensions() Object.seal() Organization Wide Defaults Override PDF Reader Performance performance.now() Permission Sets Picklist Platform events Popup Postman Primitive Types Profiles Promise propertyIsEnumerable() prototype Query Selectivity Queueable Record types Reference Types Regex Regular Expressions Relationships Rest API Rest Operator Revealing Module Pattern Role Hierarchy Salesforce Salesforce Security Schema.DescribeFieldResult Schema.DescribeSObjectResult Schema.PicklistEntry Schema.SObjectField Schema.SObjectType Security Service Components Shadow DOM Sharing Sharing Rules Singleton Slots SOAP API SOAP Web Services SOQL SOQL injection Spread Operator Star Rating stripInaccessible svg svgIcon Synchronous this Token Triggers uiObjectInfoApi Upload Files VSCode Web Services XHR
Scroll To Top