/** * @description Handles request validation and envelope JSON building for Docusign composite envelopes * @author Paul Huliganga * @date 2026-02-25 */ global with sharing class DocusignEnvelopeRequestHandler { /** * @description Validates composite envelope request parameters * @param req Request object to validate * @throws IllegalArgumentException if validation fails */ public static void validateRequest(DocusignEnvelopeRequest 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 Builds composite envelope JSON for Docusign API from request * @param req Request object containing parameters * @return JSON string for Docusign API request */ public static String buildEnvelopeJSON(DocusignEnvelopeRequest req) { // Remove duplicates and sort alphabetically List sortedTemplateIds = sortTemplatesAlphabetically( new Set(req.templateIds) ); // Build composite envelope JSON return buildCompositeEnvelopeJSON( sortedTemplateIds, req.recordId, req.language, req.emailSubject, null // customFields not supported in InvocableVariable (Phase 2 enhancement) ); } /** * @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 */ 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 } } }; } }