From 30fc345bb040682968835788c5ee7b6a53d3ecad Mon Sep 17 00:00:00 2001 From: Paul Huliganga Date: Tue, 3 Mar 2026 23:10:10 -0500 Subject: [PATCH] Sort templates alphabetically by Short_Name__c in email subject and document order - Updated DocusignCompositeEnvelopeBuilder.cls to sort documents and subject by Short_Name__c - Modified Docusign_Envelope_Templates_V3.flow-meta.xml: repositioned elements and added filter for Short_Name__c not null --- .../DocusignCompositeEnvelopeBuilder.cls | 17 +- ...ocusignCompositeEnvelopeBuilder.cls.backup | 251 ++++++++++++++++++ ...cusign_Envelope_Templates_V3.flow-meta.xml | 41 +-- 3 files changed, 289 insertions(+), 20 deletions(-) create mode 100644 composite-envelope-builder/force-app/main/default/classes/DocusignCompositeEnvelopeBuilder.cls.backup diff --git a/composite-envelope-builder/force-app/main/default/classes/DocusignCompositeEnvelopeBuilder.cls b/composite-envelope-builder/force-app/main/default/classes/DocusignCompositeEnvelopeBuilder.cls index de6f2d6..405c814 100644 --- a/composite-envelope-builder/force-app/main/default/classes/DocusignCompositeEnvelopeBuilder.cls +++ b/composite-envelope-builder/force-app/main/default/classes/DocusignCompositeEnvelopeBuilder.cls @@ -65,6 +65,7 @@ global with sharing class DocusignCompositeEnvelopeBuilder { List documents = new List(); List docNames = new List(); + Map labelToId = new Map(); for (String templateId : sortedTemplateIds) { String label; if (templateShortNames.containsKey(templateId)) { @@ -74,13 +75,23 @@ global with sharing class DocusignCompositeEnvelopeBuilder { } else { label = templateId; } + labelToId.put(label, templateId); + docNames.add(label); + } + docNames.sort(); + List sortedIds = new List(); + 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( dfsle.Document.fromTemplate( dfsle.UUID.parse(templateId), label ) ); - docNames.add(label); } 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 if (!documents.isEmpty()) { documents[0] = dfsle.Document.fromTemplate( - dfsle.UUID.parse(sortedTemplateIds[0]), + dfsle.UUID.parse(sortedIds[0]), combinedName ); myEnvelope = myEnvelope.withDocuments(documents); @@ -116,7 +127,7 @@ global with sharing class DocusignCompositeEnvelopeBuilder { } } List bodyParts = new List(); - for (String templateId : sortedTemplateIds) { + for (String templateId : sortedIds) { if (templateBodies.containsKey(templateId)) { bodyParts.add(templateBodies.get(templateId)); } diff --git a/composite-envelope-builder/force-app/main/default/classes/DocusignCompositeEnvelopeBuilder.cls.backup b/composite-envelope-builder/force-app/main/default/classes/DocusignCompositeEnvelopeBuilder.cls.backup new file mode 100644 index 0000000..c5c26a5 --- /dev/null +++ b/composite-envelope-builder/force-app/main/default/classes/DocusignCompositeEnvelopeBuilder.cls.backup @@ -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 sendCompositeEnvelope(List requests) { + List results = new List(); + + 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 sortedTemplateIds = new List(new Set(req.templateIds)); + sortedTemplateIds.sort(); + + // Query template names for document labels (shows in Docusign Status) + Map templateNames = new Map(); + 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 documents = new List(); + List docNames = new List(); + 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 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 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 recipients = new List(); + 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{ + ' - 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 buildErrorResult(String errorMessage) { + DocusignEnvelopeResult result = new DocusignEnvelopeResult(); + result.success = false; + result.errorMessage = errorMessage; + result.envelopeId = null; + return new List{ result }; + } +} diff --git a/composite-envelope-builder/force-app/main/default/flows/Docusign_Envelope_Templates_V3.flow-meta.xml b/composite-envelope-builder/force-app/main/default/flows/Docusign_Envelope_Templates_V3.flow-meta.xml index f7c980b..b47d4f2 100644 --- a/composite-envelope-builder/force-app/main/default/flows/Docusign_Envelope_Templates_V3.flow-meta.xml +++ b/composite-envelope-builder/force-app/main/default/flows/Docusign_Envelope_Templates_V3.flow-meta.xml @@ -3,8 +3,8 @@ Send_Composite_Envelope - 50 - 1000 + 182 + 1082 DocusignCompositeEnvelopeBuilder apex @@ -49,7 +49,7 @@ Add_Template_ID - 50 + 270 890 compositeTemplateIds @@ -65,8 +65,8 @@ Check_Envelope_Result - 50 - 1108 + 182 + 1190 Error_Screen @@ -90,7 +90,7 @@ Check_Row_Selection - 182 + 380 674 Row_not_selected @@ -115,7 +115,7 @@ Is_Language_Selected - 380 + 611 242 Language_Not_Added_Screen @@ -143,7 +143,7 @@ Build_Template_ID_Collection - 50 + 182 782 data.selectedRows Asc @@ -176,7 +176,7 @@ DocuSign_Envelope_Templates - 182 + 380 458 false @@ -190,6 +190,13 @@ Get_Records.Docusign_Envelope_Language__c + + Short_Name__c + IsNull + + false + + false dfsle__EnvelopeConfiguration__c Id @@ -202,7 +209,7 @@ Get_Records - 380 + 611 134 false @@ -225,7 +232,7 @@ Envelope_template_records - 182 + 380 566 true true @@ -292,7 +299,7 @@ Error_Screen 314 - 1216 + 1298 true true false @@ -316,7 +323,7 @@ Language_Not_Added_Screen - 578 + 842 350 false true @@ -340,7 +347,7 @@ Language_Warning_Screen - 182 + 380 350 false true @@ -368,7 +375,7 @@ Row_not_selected - 314 + 578 782 true true @@ -394,7 +401,7 @@ Success_Screen 50 - 1216 + 1298 false true false @@ -415,7 +422,7 @@ false - 254 + 485 0 Get_Records