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.
- Navigate to Setup.
- Enter “Sharing Settings” in the Quick Find box, then click
Sharing Settings
. - Under
SBQQ__Quote__c
, clickNew
in theCustom Sharing Reasons
section. - 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.
Wow thanks a lot Dennis,
I was exactly looking for this use-case and here it is !
Just curious, is there a reason this is limited to Apex or is it possible to accomplish with Flow?
If you have access to the _share objects in flow, sure you could do it with flow too. 👍🏻