/** * @description Main invocable class for combining multiple Docusign templates into a single envelope * @author Paul Huliganga * @date 2026-02-23 */ global with sharing class DocusignCompositeEnvelopeBuilder { /** * @description Invocable method called from Salesforce Screen Flow * @param requests List of request objects containing template IDs and metadata * @return List of result objects with envelope ID and status */ @InvocableMethod( label='Send Composite Docusign Envelope' description='Combines multiple Docusign templates into a single envelope' category='Docusign' ) public static List sendCompositeEnvelope(List requests) { List results = new List(); // Process first request (Flow only sends one) if (requests == null || requests.isEmpty()) { return buildErrorResult('No request provided'); } Request req = requests[0]; Result result = new Result(); try { // Validate inputs validateInputs(req); // Remove duplicates and sort alphabetically List sortedTemplateIds = sortTemplatesAlphabetically( new Set(req.templateIds) ); // Build composite envelope JSON String envelopeJSON = buildCompositeEnvelopeJSON( sortedTemplateIds, req.recordId, req.language, req.emailSubject, null // customFields not supported in InvocableVariable (Phase 2 enhancement) ); // Get Docusign credentials DocusignCredentials creds = DocusignCredentials.getInstance(); // Call Docusign API String envelopeId = DocusignAPIService.createCompositeEnvelope( envelopeJSON, creds ); // Success result.envelopeId = envelopeId; result.success = true; result.errorMessage = null; // Log success logAPICall(req.templateIds.size(), envelopeId, 'Success', null); } catch (Exception e) { // Error handling result.success = false; result.errorMessage = e.getMessage(); result.envelopeId = null; // Log error logAPICall( req.templateIds != null ? req.templateIds.size() : 0, null, 'Error', e.getMessage() + '\n' + e.getStackTraceString() ); // Re-throw if critical (governor limits) if (e instanceof System.LimitException) { throw e; } } results.add(result); return results; } /** * @description Validates input parameters * @param req Request object to validate * @throws IllegalArgumentException if validation fails */ private static void validateInputs(Request req) { if (req.templateIds == null || req.templateIds.isEmpty()) { throw new IllegalArgumentException('At least one template ID is required'); } if (req.templateIds.size() > 14) { throw new IllegalArgumentException('Maximum 14 templates allowed per envelope'); } if (String.isBlank(req.recordId)) { throw new IllegalArgumentException('Salesforce record ID is required'); } // Check for null template IDs for (String templateId : req.templateIds) { if (String.isBlank(templateId)) { throw new IllegalArgumentException('Template ID cannot be blank'); } } } /** * @description Removes duplicates and sorts template IDs alphabetically * @param templateIdSet Set of template IDs * @return Sorted list of unique template IDs */ private static List sortTemplatesAlphabetically(Set templateIdSet) { List sortedList = new List(templateIdSet); sortedList.sort(); return sortedList; } /** * @description Builds composite envelope JSON for Docusign API * @param templateIds List of template IDs to combine * @param recordId Salesforce record ID for custom fields * @param language Language code (en/es) * @param emailSubject Email subject line * @param customFields Map of custom field name/value pairs * @return JSON string for Docusign API request */ @TestVisible private static String buildCompositeEnvelopeJSON( List templateIds, String recordId, String language, String emailSubject, Map customFields ) { // Build composite templates array List compositeTemplates = new List(); Integer sequence = 1; for (String templateId : templateIds) { Map compositeTemplate = new Map{ 'compositeTemplateId' => String.valueOf(sequence), 'serverTemplates' => new List{ new Map{ 'sequence' => String.valueOf(sequence), 'templateId' => templateId } } }; // Add custom fields if this is the first template if (sequence == 1 && (String.isNotBlank(recordId) || customFields != null)) { compositeTemplate.put('inlineTemplates', buildInlineTemplates(recordId, language, customFields)); } compositeTemplates.add(compositeTemplate); sequence++; } // Build envelope object Map envelope = new Map{ 'status' => 'sent', 'emailSubject' => String.isNotBlank(emailSubject) ? emailSubject : 'Please review and sign these forms', 'compositeTemplates' => compositeTemplates }; return JSON.serialize(envelope); } /** * @description Builds inline templates for custom fields * @param recordId Salesforce record ID * @param language Language code * @param customFields Additional custom fields * @return List of inline template objects */ private static List buildInlineTemplates( String recordId, String language, Map customFields ) { List textCustomFields = new List(); // Add Salesforce record ID if (String.isNotBlank(recordId)) { textCustomFields.add(new Map{ 'name' => 'SalesforceRecordId', 'value' => recordId, 'show' => 'false', 'required' => 'false' }); } // Add language if (String.isNotBlank(language)) { textCustomFields.add(new Map{ 'name' => 'Language', 'value' => language, 'show' => 'false', 'required' => 'false' }); } // Add additional custom fields if (customFields != null && !customFields.isEmpty()) { for (String fieldName : customFields.keySet()) { textCustomFields.add(new Map{ 'name' => fieldName, 'value' => customFields.get(fieldName), 'show' => 'false', 'required' => 'false' }); } } return new List{ new Map{ 'sequence' => '1', 'customFields' => new Map{ 'textCustomFields' => textCustomFields } } }; } /** * @description Logs API call to debug log (future: custom object) * @param templateCount Number of templates in envelope * @param envelopeId Docusign envelope ID * @param status Success or Error * @param errorMessage Error message if applicable */ private static void logAPICall( Integer templateCount, String envelopeId, String status, String errorMessage ) { System.debug(LoggingLevel.INFO, '=== Docusign Composite Envelope API Call ==='); System.debug(LoggingLevel.INFO, 'Timestamp: ' + System.now()); System.debug(LoggingLevel.INFO, 'Template Count: ' + templateCount); System.debug(LoggingLevel.INFO, 'Envelope ID: ' + envelopeId); System.debug(LoggingLevel.INFO, 'Status: ' + status); if (String.isNotBlank(errorMessage)) { System.debug(LoggingLevel.ERROR, 'Error: ' + errorMessage); } // Future enhancement: Insert into Docusign_API_Log__c custom object } /** * @description Helper to build error result * @param errorMessage Error message * @return List containing single error result */ private static List buildErrorResult(String errorMessage) { Result result = new Result(); result.success = false; result.errorMessage = errorMessage; result.envelopeId = null; return new List{ result }; } /** * @description Input parameters for invocable method (from Screen Flow) */ global class Request { @InvocableVariable( label='Template IDs' description='List of Docusign template IDs to combine' required=true ) public List templateIds; @InvocableVariable( label='Salesforce Record ID' description='ID of the Salesforce record to attach documents to' required=true ) public String recordId; @InvocableVariable( label='Language' description='Language code (en or es)' required=false ) public String language; @InvocableVariable( label='Email Subject' description='Subject line for envelope email' required=false ) public String emailSubject; } /** * @description Output parameters for invocable method (to Screen Flow) */ global class Result { @InvocableVariable( label='Envelope ID' description='Docusign envelope ID' ) public String envelopeId; @InvocableVariable( label='Success' description='True if envelope was created successfully' ) public Boolean success; @InvocableVariable( label='Error Message' description='Error message if envelope creation failed' ) public String errorMessage; } }