Sort templates alphabetically by Short_Name__c in email subject and document order

This commit is contained in:
Paul Huliganga 2026-03-03 23:10:10 -05:00
parent 61446bde23
commit 6851bb4632
3 changed files with 289 additions and 20 deletions

View File

@ -65,6 +65,7 @@ global with sharing class DocusignCompositeEnvelopeBuilder {
List<dfsle.Document> documents = new List<dfsle.Document>(); List<dfsle.Document> documents = new List<dfsle.Document>();
List<String> docNames = new List<String>(); List<String> docNames = new List<String>();
Map<String, String> labelToId = new Map<String, String>();
for (String templateId : sortedTemplateIds) { for (String templateId : sortedTemplateIds) {
String label; String label;
if (templateShortNames.containsKey(templateId)) { if (templateShortNames.containsKey(templateId)) {
@ -74,13 +75,23 @@ global with sharing class DocusignCompositeEnvelopeBuilder {
} else { } else {
label = templateId; label = templateId;
} }
labelToId.put(label, templateId);
docNames.add(label);
}
docNames.sort();
List<String> sortedIds = new List<String>();
for (String label : docNames) {
sortedIds.add(labelToId.get(label));
}
for (Integer i = 0; i < sortedIds.size(); i++) {
String templateId = sortedIds[i];
String label = docNames[i];
documents.add( documents.add(
dfsle.Document.fromTemplate( dfsle.Document.fromTemplate(
dfsle.UUID.parse(templateId), dfsle.UUID.parse(templateId),
label label
) )
); );
docNames.add(label);
} }
myEnvelope = myEnvelope.withDocuments(documents); myEnvelope = myEnvelope.withDocuments(documents);
@ -93,7 +104,7 @@ global with sharing class DocusignCompositeEnvelopeBuilder {
// Use combined name as the first document label so it appears in Status // Use combined name as the first document label so it appears in Status
if (!documents.isEmpty()) { if (!documents.isEmpty()) {
documents[0] = dfsle.Document.fromTemplate( documents[0] = dfsle.Document.fromTemplate(
dfsle.UUID.parse(sortedTemplateIds[0]), dfsle.UUID.parse(sortedIds[0]),
combinedName combinedName
); );
myEnvelope = myEnvelope.withDocuments(documents); myEnvelope = myEnvelope.withDocuments(documents);
@ -116,7 +127,7 @@ global with sharing class DocusignCompositeEnvelopeBuilder {
} }
} }
List<String> bodyParts = new List<String>(); List<String> bodyParts = new List<String>();
for (String templateId : sortedTemplateIds) { for (String templateId : sortedIds) {
if (templateBodies.containsKey(templateId)) { if (templateBodies.containsKey(templateId)) {
bodyParts.add(templateBodies.get(templateId)); bodyParts.add(templateBodies.get(templateId));
} }

View File

@ -0,0 +1,251 @@
/**
* @description Combines multiple Docusign templates into a single composite envelope
* using the dfsle Apex Toolkit (Docusign for Salesforce managed package).
* Recipients are resolved from Client_Case__c lookup fields.
* @author Paul Huliganga
* @date 2026-02-25
*/
global with sharing class DocusignCompositeEnvelopeBuilder {
// ============================================================
// CONFIGURATION: Update these constants if field/role names change
// ============================================================
// API names of the lookup fields on Client_Case__c that point to recipient records
// These are the "Select Lookup Field" values from the Docusign template recipient config
private static final String FIELD_SERVICE_COORDINATOR = 'Service_Coordinator__c';
private static final String FIELD_DOCUSIGN_RECIPIENT = 'Docusign_Recipient_1__c';
// Role names must match EXACTLY what's configured in the Docusign templates
private static final String ROLE_SERVICE_COORDINATOR = 'Service Coordinator';
private static final String ROLE_DOCUSIGN_RECIPIENT = 'Docusign Recipient #1';
@InvocableMethod(
label='Send Composite Docusign Envelope'
description='Combines multiple Docusign templates into a single envelope using dfsle Apex Toolkit'
category='Docusign'
)
public static List<DocusignEnvelopeResult> sendCompositeEnvelope(List<DocusignEnvelopeRequest> requests) {
List<DocusignEnvelopeResult> results = new List<DocusignEnvelopeResult>();
if (requests == null || requests.isEmpty()) {
return buildErrorResult('No request provided');
}
DocusignEnvelopeRequest req = requests[0];
DocusignEnvelopeResult result = new DocusignEnvelopeResult();
try {
// Validate request
DocusignEnvelopeRequestHandler.validateRequest(req);
// Create empty envelope linked to the source record
dfsle.Envelope myEnvelope = dfsle.EnvelopeService.getEmptyEnvelope(
new dfsle.Entity(req.recordId)
);
// Build document list from templates (deduplicated and sorted)
List<String> sortedTemplateIds = new List<String>(new Set<String>(req.templateIds));
sortedTemplateIds.sort();
// Query template names for document labels (shows in Docusign Status)
Map<String, String> templateNames = new Map<String, String>();
for (dfsle__EnvelopeConfiguration__c config : [
SELECT dfsle__DocuSignId__c, Name
FROM dfsle__EnvelopeConfiguration__c
WHERE dfsle__DocuSignId__c IN :sortedTemplateIds
]) {
templateNames.put(config.dfsle__DocuSignId__c, config.Name);
}
List<dfsle.Document> documents = new List<dfsle.Document>();
List<String> docNames = new List<String>();
for (String templateId : sortedTemplateIds) {
String label = templateNames.containsKey(templateId)
? stripLanguageSuffix(templateNames.get(templateId))
: templateId;
documents.add(
dfsle.Document.fromTemplate(
dfsle.UUID.parse(templateId),
label
)
);
docNames.add(label);
}
myEnvelope = myEnvelope.withDocuments(documents);
// Set combined template names as the envelope document name
// (shows in Docusign Status "Document Name" column)
String combinedName = String.join(docNames, ', ');
if (combinedName.length() > 255) {
combinedName = combinedName.left(252) + '...';
}
// Use combined name as the first document label so it appears in Status
if (!documents.isEmpty()) {
documents[0] = dfsle.Document.fromTemplate(
dfsle.UUID.parse(sortedTemplateIds[0]),
combinedName
);
myEnvelope = myEnvelope.withDocuments(documents);
}
// Resolve recipients from Client_Case__c lookup fields
List<dfsle.Recipient> recipients = resolveRecipients(req.recordId);
myEnvelope = myEnvelope.withRecipients(recipients);
// Set email subject if provided
if (String.isNotBlank(req.emailSubject)) {
myEnvelope = myEnvelope.withEmail(req.emailSubject, '');
}
// Send the envelope
myEnvelope = dfsle.EnvelopeService.sendEnvelope(myEnvelope, true);
// Success
result.envelopeId = String.valueOf(myEnvelope.docuSignId);
result.success = true;
result.errorMessage = null;
logResult(sortedTemplateIds.size(), result.envelopeId, 'Success (' + String.join(docNames, ', ') + ')', null);
} catch (Exception e) {
result.success = false;
result.errorMessage = e.getMessage();
result.envelopeId = null;
logResult(
req.templateIds != null ? req.templateIds.size() : 0,
null, 'Error',
e.getMessage() + '\n' + e.getStackTraceString()
);
if (e instanceof System.LimitException) {
throw e;
}
}
results.add(result);
return results;
}
/**
* @description Resolves recipients from Client_Case__c lookup fields.
* Queries the case record and related contacts to get name/email.
* @param recordId The Client_Case__c record ID
* @return List of dfsle.Recipient objects with role mappings
*/
private static List<dfsle.Recipient> resolveRecipients(String recordId) {
// Query the Client_Case__c record with recipient lookup fields
// NOTE: Adjust field API names if they differ in your org
String query = 'SELECT Id, '
+ FIELD_SERVICE_COORDINATOR + ', '
+ FIELD_DOCUSIGN_RECIPIENT
+ ' FROM Client_Case__c WHERE Id = :recordId LIMIT 1';
Client_Case__c caseRecord = Database.query(query);
List<dfsle.Recipient> recipients = new List<dfsle.Recipient>();
Integer routingOrder = 1;
// Recipient 1: Service Coordinator
Id serviceCoordinatorId = (Id) caseRecord.get(FIELD_SERVICE_COORDINATOR);
if (serviceCoordinatorId != null) {
recipients.add(buildRecipient(serviceCoordinatorId, ROLE_SERVICE_COORDINATOR, routingOrder++, recordId));
}
// Recipient 2: Docusign Recipient #1
Id docusignRecipientId = (Id) caseRecord.get(FIELD_DOCUSIGN_RECIPIENT);
if (docusignRecipientId != null) {
recipients.add(buildRecipient(docusignRecipientId, ROLE_DOCUSIGN_RECIPIENT, routingOrder++, recordId));
}
if (recipients.isEmpty()) {
throw new IllegalArgumentException('No recipients found on the Client Case record. '
+ 'Please ensure Service Coordinator and Docusign Recipient #1 are populated.');
}
return recipients;
}
/**
* @description Builds a dfsle.Recipient from a Contact/User lookup ID
* @param recipientId The Contact or User record ID
* @param roleName The Docusign template role name
* @param routingOrder Signing order
* @param sourceRecordId The source Client_Case__c record ID
* @return dfsle.Recipient configured for the role
*/
private static dfsle.Recipient buildRecipient(Id recipientId, String roleName, Integer routingOrder, String sourceRecordId) {
// Determine if this is a Contact or User
String objectType = recipientId.getSObjectType().getDescribe().getName();
String recipientName;
String recipientEmail;
if (objectType == 'Contact') {
Contact c = [SELECT Id, Name, Email FROM Contact WHERE Id = :recipientId LIMIT 1];
recipientName = c.Name;
recipientEmail = c.Email;
} else if (objectType == 'User') {
User u = [SELECT Id, Name, Email FROM User WHERE Id = :recipientId LIMIT 1];
recipientName = u.Name;
recipientEmail = u.Email;
} else {
throw new IllegalArgumentException('Unsupported recipient type: ' + objectType
+ '. Expected Contact or User.');
}
if (String.isBlank(recipientEmail)) {
throw new IllegalArgumentException('No email found for ' + roleName + ' (' + recipientName + '). '
+ 'Please ensure the recipient has a valid email address.');
}
return dfsle.Recipient.fromSource(
recipientName,
recipientEmail,
null, // phone (optional)
roleName, // must match template role exactly
new dfsle.Entity(sourceRecordId) // source record for merge fields
);
}
private static void logResult(Integer templateCount, String envelopeId, String status, String errorMessage) {
System.debug(LoggingLevel.INFO, '=== Docusign Composite Envelope ===');
System.debug(LoggingLevel.INFO, 'Templates: ' + templateCount);
System.debug(LoggingLevel.INFO, 'Envelope ID: ' + envelopeId);
System.debug(LoggingLevel.INFO, 'Status: ' + status);
if (String.isNotBlank(errorMessage)) {
System.debug(LoggingLevel.ERROR, 'Error: ' + errorMessage);
}
}
/**
* @description Strips language suffixes like " - English" or " - Spanish" from template names
* @param name Template name
* @return Cleaned template name
*/
@TestVisible
private static String stripLanguageSuffix(String name) {
if (String.isBlank(name)) return name;
// Remove common language suffixes (case-insensitive)
String cleaned = name;
for (String suffix : new List<String>{
' - English', ' - Spanish', ' - French',
' - Anglais', ' - Espagnol', ' - Français'
}) {
if (cleaned.endsWithIgnoreCase(suffix)) {
cleaned = cleaned.left(cleaned.length() - suffix.length());
break;
}
}
return cleaned.trim();
}
private static List<DocusignEnvelopeResult> buildErrorResult(String errorMessage) {
DocusignEnvelopeResult result = new DocusignEnvelopeResult();
result.success = false;
result.errorMessage = errorMessage;
result.envelopeId = null;
return new List<DocusignEnvelopeResult>{ result };
}
}

View File

@ -3,8 +3,8 @@
<actionCalls> <actionCalls>
<name>Send_Composite_Envelope</name> <name>Send_Composite_Envelope</name>
<label>Send Composite Envelope</label> <label>Send Composite Envelope</label>
<locationX>50</locationX> <locationX>182</locationX>
<locationY>1000</locationY> <locationY>1082</locationY>
<actionName>DocusignCompositeEnvelopeBuilder</actionName> <actionName>DocusignCompositeEnvelopeBuilder</actionName>
<actionType>apex</actionType> <actionType>apex</actionType>
<connector> <connector>
@ -49,7 +49,7 @@
<assignments> <assignments>
<name>Add_Template_ID</name> <name>Add_Template_ID</name>
<label>Add Template ID</label> <label>Add Template ID</label>
<locationX>50</locationX> <locationX>270</locationX>
<locationY>890</locationY> <locationY>890</locationY>
<assignmentItems> <assignmentItems>
<assignToReference>compositeTemplateIds</assignToReference> <assignToReference>compositeTemplateIds</assignToReference>
@ -65,8 +65,8 @@
<decisions> <decisions>
<name>Check_Envelope_Result</name> <name>Check_Envelope_Result</name>
<label>Check Envelope Result</label> <label>Check Envelope Result</label>
<locationX>50</locationX> <locationX>182</locationX>
<locationY>1108</locationY> <locationY>1190</locationY>
<defaultConnector> <defaultConnector>
<targetReference>Error_Screen</targetReference> <targetReference>Error_Screen</targetReference>
</defaultConnector> </defaultConnector>
@ -90,7 +90,7 @@
<decisions> <decisions>
<name>Check_Row_Selection</name> <name>Check_Row_Selection</name>
<label>Check Row Selection</label> <label>Check Row Selection</label>
<locationX>182</locationX> <locationX>380</locationX>
<locationY>674</locationY> <locationY>674</locationY>
<defaultConnector> <defaultConnector>
<targetReference>Row_not_selected</targetReference> <targetReference>Row_not_selected</targetReference>
@ -115,7 +115,7 @@
<decisions> <decisions>
<name>Is_Language_Selected</name> <name>Is_Language_Selected</name>
<label>Is Language Selected?</label> <label>Is Language Selected?</label>
<locationX>380</locationX> <locationX>611</locationX>
<locationY>242</locationY> <locationY>242</locationY>
<defaultConnector> <defaultConnector>
<targetReference>Language_Not_Added_Screen</targetReference> <targetReference>Language_Not_Added_Screen</targetReference>
@ -143,7 +143,7 @@
<loops> <loops>
<name>Build_Template_ID_Collection</name> <name>Build_Template_ID_Collection</name>
<label>Build Template ID Collection</label> <label>Build Template ID Collection</label>
<locationX>50</locationX> <locationX>182</locationX>
<locationY>782</locationY> <locationY>782</locationY>
<collectionReference>data.selectedRows</collectionReference> <collectionReference>data.selectedRows</collectionReference>
<iterationOrder>Asc</iterationOrder> <iterationOrder>Asc</iterationOrder>
@ -176,7 +176,7 @@
<recordLookups> <recordLookups>
<name>DocuSign_Envelope_Templates</name> <name>DocuSign_Envelope_Templates</name>
<label>DocuSign Envelope Templates</label> <label>DocuSign Envelope Templates</label>
<locationX>182</locationX> <locationX>380</locationX>
<locationY>458</locationY> <locationY>458</locationY>
<assignNullValuesIfNoRecordsFound>false</assignNullValuesIfNoRecordsFound> <assignNullValuesIfNoRecordsFound>false</assignNullValuesIfNoRecordsFound>
<connector> <connector>
@ -190,6 +190,13 @@
<elementReference>Get_Records.Docusign_Envelope_Language__c</elementReference> <elementReference>Get_Records.Docusign_Envelope_Language__c</elementReference>
</value> </value>
</filters> </filters>
<filters>
<field>Short_Name__c</field>
<operator>IsNull</operator>
<value>
<booleanValue>false</booleanValue>
</value>
</filters>
<getFirstRecordOnly>false</getFirstRecordOnly> <getFirstRecordOnly>false</getFirstRecordOnly>
<object>dfsle__EnvelopeConfiguration__c</object> <object>dfsle__EnvelopeConfiguration__c</object>
<queriedFields>Id</queriedFields> <queriedFields>Id</queriedFields>
@ -202,7 +209,7 @@
<recordLookups> <recordLookups>
<name>Get_Records</name> <name>Get_Records</name>
<label>Get Records</label> <label>Get Records</label>
<locationX>380</locationX> <locationX>611</locationX>
<locationY>134</locationY> <locationY>134</locationY>
<assignNullValuesIfNoRecordsFound>false</assignNullValuesIfNoRecordsFound> <assignNullValuesIfNoRecordsFound>false</assignNullValuesIfNoRecordsFound>
<connector> <connector>
@ -225,7 +232,7 @@
<screens> <screens>
<name>Envelope_template_records</name> <name>Envelope_template_records</name>
<label>Envelope template records</label> <label>Envelope template records</label>
<locationX>182</locationX> <locationX>380</locationX>
<locationY>566</locationY> <locationY>566</locationY>
<allowBack>true</allowBack> <allowBack>true</allowBack>
<allowFinish>true</allowFinish> <allowFinish>true</allowFinish>
@ -292,7 +299,7 @@
<name>Error_Screen</name> <name>Error_Screen</name>
<label>Error Screen</label> <label>Error Screen</label>
<locationX>314</locationX> <locationX>314</locationX>
<locationY>1216</locationY> <locationY>1298</locationY>
<allowBack>true</allowBack> <allowBack>true</allowBack>
<allowFinish>true</allowFinish> <allowFinish>true</allowFinish>
<allowPause>false</allowPause> <allowPause>false</allowPause>
@ -316,7 +323,7 @@
<screens> <screens>
<name>Language_Not_Added_Screen</name> <name>Language_Not_Added_Screen</name>
<label>Language Not Added Screen</label> <label>Language Not Added Screen</label>
<locationX>578</locationX> <locationX>842</locationX>
<locationY>350</locationY> <locationY>350</locationY>
<allowBack>false</allowBack> <allowBack>false</allowBack>
<allowFinish>true</allowFinish> <allowFinish>true</allowFinish>
@ -340,7 +347,7 @@
<screens> <screens>
<name>Language_Warning_Screen</name> <name>Language_Warning_Screen</name>
<label>Language Warning Screen</label> <label>Language Warning Screen</label>
<locationX>182</locationX> <locationX>380</locationX>
<locationY>350</locationY> <locationY>350</locationY>
<allowBack>false</allowBack> <allowBack>false</allowBack>
<allowFinish>true</allowFinish> <allowFinish>true</allowFinish>
@ -368,7 +375,7 @@
<screens> <screens>
<name>Row_not_selected</name> <name>Row_not_selected</name>
<label>Row not selected</label> <label>Row not selected</label>
<locationX>314</locationX> <locationX>578</locationX>
<locationY>782</locationY> <locationY>782</locationY>
<allowBack>true</allowBack> <allowBack>true</allowBack>
<allowFinish>true</allowFinish> <allowFinish>true</allowFinish>
@ -394,7 +401,7 @@
<name>Success_Screen</name> <name>Success_Screen</name>
<label>Success Screen</label> <label>Success Screen</label>
<locationX>50</locationX> <locationX>50</locationX>
<locationY>1216</locationY> <locationY>1298</locationY>
<allowBack>false</allowBack> <allowBack>false</allowBack>
<allowFinish>true</allowFinish> <allowFinish>true</allowFinish>
<allowPause>false</allowPause> <allowPause>false</allowPause>
@ -415,7 +422,7 @@
<showHeader>false</showHeader> <showHeader>false</showHeader>
</screens> </screens>
<start> <start>
<locationX>254</locationX> <locationX>485</locationX>
<locationY>0</locationY> <locationY>0</locationY>
<connector> <connector>
<targetReference>Get_Records</targetReference> <targetReference>Get_Records</targetReference>