/** * @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 and short names for document labels and email subject Map templateNames = new Map(); Map templateShortNames = new Map(); for (dfsle__EnvelopeConfiguration__c config : [ SELECT dfsle__DocuSignId__c, Name, Short_Name__c FROM dfsle__EnvelopeConfiguration__c WHERE dfsle__DocuSignId__c IN :sortedTemplateIds ]) { templateNames.put(config.dfsle__DocuSignId__c, config.Name); if (String.isNotBlank(config.Short_Name__c)) { templateShortNames.put(config.dfsle__DocuSignId__c, config.Short_Name__c); } } List documents = new List(); List docNames = new List(); List subjectNames = 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); // Use short name for email subject if available, otherwise use regular name String subjectName = templateShortNames.containsKey(templateId) ? templateShortNames.get(templateId) : label; subjectNames.add(subjectName); } 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 to combined template short names (or custom subject if provided) String emailSubject = String.isNotBlank(req.emailSubject) ? req.emailSubject : String.join(subjectNames, ', '); // Truncate to 100 characters maximum as required by Docusign if (emailSubject.length() > 100) { emailSubject = emailSubject.left(97) + '...'; } myEnvelope = myEnvelope.withEmail(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 }; } }