/** * @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(); List documents = new List(); for (String templateId : sortedTemplateIds) { documents.add( dfsle.Document.fromTemplate( dfsle.UUID.parse(templateId), 'Template ' + templateId.left(8) ) ); } 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', 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); } } private static List buildErrorResult(String errorMessage) { DocusignEnvelopeResult result = new DocusignEnvelopeResult(); result.success = false; result.errorMessage = errorMessage; result.envelopeId = null; return new List{ result }; } }