Salesforce CPQ: Sharing Quotes based on Opportunity Team Members

Let’s think about the concept of using Apex to create bulk sharing operations, which is a fundamental aspect of implementing Apex Managed Sharing in Salesforce. Apex Managed Sharing is designed to programmatically share records when declarative sharing rules do not suffice, especially for custom objects or complex sharing requirements that can’t be addressed through Salesforce’s standard sharing model.

Here’s how you can apply the concepts from this concept to share Salesforce CPQ (SBQQ__Quote__c) quotes based on Opportunity Team Members:

Step 1: Define a Custom Share Reason (If Needed)

First, if you want to track why a record was shared for reporting or auditing purposes, you may define a custom share reason on the SBQQ__Quote__Share object. This is optional but can be helpful for clarity.

  1. Navigate to Setup.
  2. Enter “Sharing Settings” in the Quick Find box, then click Sharing Settings.
  3. Under SBQQ__Quote__c, click New in the Custom Sharing Reasons section.
  4. Define your custom reason (e.g., “SharedWithOpportunityTeam”).

Step 2: Implement Apex Managed Sharing

Using Apex to programmatically share SBQQ__Quote__c records involves creating SBQQ__Quote__Share records, which are used to define sharing access between SBQQ__Quote__c records and users or groups.

public class CPQQuoteSharingService {

    public class Request {
        @InvocableVariable(required = true)
        public Id opportunityId;
    }

    public class Result {
        @InvocableVariable
        public String message;
    }

    @InvocableMethod(label = 'Share Quotes With Opportunity Team' description = 'Shares CPQ quotes with the members of the opportunity team and returns operation results.')
    public static List<Result> shareQuotesWithOpportunityTeam(List<Request> requests) {
        List<Result> results = new List<Result>();
        Set<Id> opportunityIds = new Set<Id>();
        for (Request req : requests) {
            opportunityIds.add(req.opportunityId);
        }

        try {
            // Fetch all quotes related to the specified opportunities
            List<SBQQ__Quote__c> quotes = [SELECT Id FROM SBQQ__Quote__c WHERE SBQQ__Opportunity2__c IN :opportunityIds];
        
            // Fetch all Opportunity Team Members for these opportunities
            List<OpportunityTeamMember> teamMembers = [SELECT UserId, OpportunityId FROM OpportunityTeamMember WHERE OpportunityId IN :opportunityIds];
        
            // List to hold the new share records
            List<SBQQ__Quote__Share> sharesToInsert = new List<SBQQ__Quote__Share>();

            // Create share records for each quote and team member combination
            for (SBQQ__Quote__c quote : quotes) {
                for (OpportunityTeamMember member : teamMembers) {
                    if (quote.SBQQ__Opportunity2__c == member.OpportunityId) {
                        SBQQ__Quote__Share share = new SBQQ__Quote__Share();
                        share.ParentId = quote.Id; // The quote to share
                        share.UserOrGroupId = member.UserId; // The user to share with
                        share.AccessLevel = 'Read'; // Set the access level as needed
                        share.RowCause = Schema.SBQQ__Quote__Share.RowCause.Manual; // Use your custom share reason if defined
                        
                        sharesToInsert.add(share);
                    }
                }
            }
        
            // Attempt to insert the share records in bulk
            if (!sharesToInsert.isEmpty()) {
                Database.SaveResult[] saveResults = Database.insert(sharesToInsert, false);

                // Check the results for errors and prepare feedback
                for (Database.SaveResult sr : saveResults) {
                    Result result = new Result();
                    if (sr.isSuccess()) {
                        result.message = 'Success';
                    } else {
                        // There were errors during the insert operation, gather them
                        String errMsg = '';
                        for(Database.Error err : sr.getErrors()) {
                            errMsg += err.getMessage() + '\n';
                        }
                        result.message = 'Error: ' + errMsg;
                    }
                    results.add(result);
                }
            }
        } catch (Exception e) {
            // Handle unexpected exceptions
            Result result = new Result();
            result.message = 'Exception: ' + e.getMessage();
            results.add(result);
        }

        // Ensure we return a result for each request, even if there's no specific message to return
        while (results.size() < requests.size()) {
            results.add(new Result());
        }

        return results;
    }
}

Step 3: Call the Sharing Method from Triggers

You’ll need to call shareQuotesWithOpportunityTeam from triggers on both the OpportunityTeamMember and SBQQ__Quote__c objects to ensure quotes are shared according to changes in team composition or quote creation.

Considerations:

  • Governor Limits: Be mindful of governor limits, especially SOQL queries and DML operations. Ensure your implementation is bulk-safe.
  • Access Level: Adjust the AccessLevel according to your specific sharing requirements (e.g., Read, Edit).
  • Testing: Rigorously test your sharing logic in a sandbox environment before deploying to production to ensure it behaves as expected under various scenarios.

Key Points

  • InvocableVariable: Each parameter that the Flow will pass to the Apex method needs to be wrapped in a class where each parameter is annotated with @InvocableVariable. This is because @InvocableMethod expects its parameters to be lists, as it’s designed to handle bulk operations.
  • Bulk Operations: The method processes a list of requests to handle bulk operations efficiently, allowing multiple opportunity IDs to be processed in a single call from the Flow.
  • Error Handling: The method should include error handling logic (not shown in the example) to gracefully deal with any issues during the insertion of share records. This could involve returning information about any failures back to the Flow for further action.

By following these steps, you can create a dynamic sharing model for Salesforce CPQ quotes that respects the composition of Opportunity Teams, aligning with the guidance provided in the Salesforce documentation on bulk sharing creation with Apex.

3 thoughts on “Salesforce CPQ: Sharing Quotes based on Opportunity Team Members

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.