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 920aed3..ee1f2c6 100644 --- a/composite-envelope-builder/force-app/main/default/classes/DocusignCompositeEnvelopeBuilder.cls +++ b/composite-envelope-builder/force-app/main/default/classes/DocusignCompositeEnvelopeBuilder.cls @@ -1,278 +1,7 @@ -/** - * @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) - // Uses Short_Name__c if populated, otherwise falls back to Name (with language suffix stripped) - 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(); - for (String templateId : sortedTemplateIds) { - String label; - if (templateShortNames.containsKey(templateId)) { - label = templateShortNames.get(templateId); - } else if (templateNames.containsKey(templateId)) { - label = stripLanguageSuffix(templateNames.get(templateId)); - } else { - label = 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 envelope subject to combined template names and body to concatenated template email messages - // Query for EmailMessage__c - Map templateBodies = new Map(); - for (dfsle__EnvelopeConfiguration__c config : [ - SELECT dfsle__DocuSignId__c, dfsle__EmailMessage__c - FROM dfsle__EnvelopeConfiguration__c - WHERE dfsle__DocuSignId__c IN :sortedTemplateIds - ]) { - if (String.isNotBlank(config.dfsle__EmailMessage__c)) { - templateBodies.put(config.dfsle__DocuSignId__c, config.dfsle__EmailMessage__c); - } - } - List bodyParts = new List(); - for (String templateId : sortedTemplateIds) { - if (templateBodies.containsKey(templateId)) { - bodyParts.add(templateBodies.get(templateId)); - } - } String envelopeSubject = combinedName; + // Truncate subject to 100 characters maximum as required by Docusign + if (envelopeSubject.length() > 100) { + envelopeSubject = envelopeSubject.left(97) + '...'; + } String envelopeBody = bodyParts.isEmpty() ? '' : String.join(bodyParts, '\n\n---\n\n'); - myEnvelope = myEnvelope.withEmail(envelopeSubject, envelopeBody); - - // 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 }; - } -} + myEnvelope = myEnvelope.withEmail(envelopeSubject, envelopeBody); \ No newline at end of file diff --git a/composite-envelope-builder/force-app/main/default/classes/DocusignCompositeEnvelopeBuilderTest.cls b/composite-envelope-builder/force-app/main/default/classes/DocusignCompositeEnvelopeBuilderTest.cls index b8c572f..ef368c1 100644 --- a/composite-envelope-builder/force-app/main/default/classes/DocusignCompositeEnvelopeBuilderTest.cls +++ b/composite-envelope-builder/force-app/main/default/classes/DocusignCompositeEnvelopeBuilderTest.cls @@ -1,266 +1,30 @@ -/** - * @description Test class for DocusignCompositeEnvelopeBuilder (dfsle Apex Toolkit) - * @author Paul Huliganga - * @date 2026-02-25 - */ -@isTest -private class DocusignCompositeEnvelopeBuilderTest { - - @isTest - static void testSuccessfulCompositeEnvelope() { - // Arrange - dfsle.TestUtils.setMock(new dfsle.ESignatureAPIMock()); - - DocusignEnvelopeRequest req = new DocusignEnvelopeRequest(); - req.templateIds = new List{ - '01234567-abcd-ef01-2345-6789abcdef01', - '01234567-abcd-ef01-2345-6789abcdef02', - '01234567-abcd-ef01-2345-6789abcdef03' - }; - req.recordId = '001000000ABC123'; - req.language = 'en'; - req.emailSubject = 'Please sign these forms'; - - // Act - Test.startTest(); - List results = - DocusignCompositeEnvelopeBuilder.sendCompositeEnvelope( - new List{req} - ); - Test.stopTest(); - - // Assert - System.assertEquals(1, results.size(), 'Should return 1 result'); - System.assertEquals(true, results[0].success, 'Should be successful'); - System.assertNotEquals(null, results[0].envelopeId, 'Should have envelope ID'); - System.assertEquals(null, results[0].errorMessage, 'Should have no error'); - } - - @isTest - static void testSingleTemplate() { - // Arrange - dfsle.TestUtils.setMock(new dfsle.ESignatureAPIMock()); - - DocusignEnvelopeRequest req = new DocusignEnvelopeRequest(); - req.templateIds = new List{'01234567-abcd-ef01-2345-6789abcdef01'}; - req.recordId = '001000000ABC123'; - - // Act - Test.startTest(); - List results = - DocusignCompositeEnvelopeBuilder.sendCompositeEnvelope( - new List{req} - ); - Test.stopTest(); - - // Assert - System.assertEquals(true, results[0].success, 'Should succeed with 1 template'); - } - - @isTest - static void testMaximumTemplates() { - // Arrange - dfsle.TestUtils.setMock(new dfsle.ESignatureAPIMock()); - - List templateIds = new List(); - for (Integer i = 1; i <= 14; i++) { - templateIds.add('01234567-abcd-ef01-2345-6789abcdef' + String.valueOf(i).leftPad(2, '0')); - } - - DocusignEnvelopeRequest req = new DocusignEnvelopeRequest(); - req.templateIds = templateIds; - req.recordId = '001000000ABC123'; - - // Act - Test.startTest(); - List results = - DocusignCompositeEnvelopeBuilder.sendCompositeEnvelope( - new List{req} - ); - Test.stopTest(); - - // Assert - System.assertEquals(true, results[0].success, 'Should succeed with 14 templates'); - } - - @isTest - static void testDuplicateTemplatesDeduped() { - // Arrange - dfsle.TestUtils.setMock(new dfsle.ESignatureAPIMock()); - - DocusignEnvelopeRequest req = new DocusignEnvelopeRequest(); - req.templateIds = new List{ - '01234567-abcd-ef01-2345-6789abcdef01', - '01234567-abcd-ef01-2345-6789abcdef02', - '01234567-abcd-ef01-2345-6789abcdef01' // duplicate - }; - req.recordId = '001000000ABC123'; - - // Act - Test.startTest(); - List results = - DocusignCompositeEnvelopeBuilder.sendCompositeEnvelope( - new List{req} - ); - Test.stopTest(); - - // Assert - System.assertEquals(true, results[0].success, 'Should handle duplicates'); - } - - @isTest - static void testNullRequest() { - // Act - Test.startTest(); - List results = - DocusignCompositeEnvelopeBuilder.sendCompositeEnvelope(null); - Test.stopTest(); - - // Assert - System.assertEquals(false, results[0].success, 'Should fail with null request'); - System.assertEquals('No request provided', results[0].errorMessage, 'Should have error message'); - } - - @isTest - static void testEmptyRequest() { - // Act - Test.startTest(); - List results = - DocusignCompositeEnvelopeBuilder.sendCompositeEnvelope( - new List() - ); - Test.stopTest(); - - // Assert - System.assertEquals(false, results[0].success, 'Should fail with empty request'); - } - - @isTest - static void testValidationNoTemplates() { - // Arrange - DocusignEnvelopeRequest req = new DocusignEnvelopeRequest(); - req.templateIds = new List(); - req.recordId = '001000000ABC123'; - - // Act - Test.startTest(); - List results = - DocusignCompositeEnvelopeBuilder.sendCompositeEnvelope( - new List{req} - ); - Test.stopTest(); - - // Assert - System.assertEquals(false, results[0].success, 'Should fail validation'); - System.assert(results[0].errorMessage.containsIgnoreCase('template'), 'Should mention templates'); - } - - @isTest - static void testValidationTooManyTemplates() { - // Arrange - List templateIds = new List(); - for (Integer i = 1; i <= 15; i++) { - templateIds.add('01234567-abcd-ef01-2345-6789abcdef' + String.valueOf(i).leftPad(2, '0')); - } - - DocusignEnvelopeRequest req = new DocusignEnvelopeRequest(); - req.templateIds = templateIds; - req.recordId = '001000000ABC123'; - - // Act - Test.startTest(); - List results = - DocusignCompositeEnvelopeBuilder.sendCompositeEnvelope( - new List{req} - ); - Test.stopTest(); - - // Assert - System.assertEquals(false, results[0].success, 'Should fail validation'); - System.assert(results[0].errorMessage.contains('Maximum 14'), 'Should mention limit'); - } - - @isTest - static void testValidationNoRecordId() { - // Arrange - DocusignEnvelopeRequest req = new DocusignEnvelopeRequest(); - req.templateIds = new List{'01234567-abcd-ef01-2345-6789abcdef01'}; - req.recordId = ''; - - // Act - Test.startTest(); - List results = - DocusignCompositeEnvelopeBuilder.sendCompositeEnvelope( - new List{req} - ); - Test.stopTest(); - - // Assert - System.assertEquals(false, results[0].success, 'Should fail validation'); - System.assert(results[0].errorMessage.contains('record ID'), 'Should mention record ID'); - } - - @isTest - static void testValidationBlankTemplateId() { - // Arrange - DocusignEnvelopeRequest req = new DocusignEnvelopeRequest(); - req.templateIds = new List{'01234567-abcd-ef01-2345-6789abcdef01', ''}; - req.recordId = '001000000ABC123'; - - // Act - Test.startTest(); - List results = - DocusignCompositeEnvelopeBuilder.sendCompositeEnvelope( - new List{req} - ); - Test.stopTest(); - - // Assert - System.assertEquals(false, results[0].success, 'Should fail validation'); - System.assert(results[0].errorMessage.contains('blank'), 'Should mention blank'); - } - - @isTest - static void testWithEmailSubject() { - // Arrange - dfsle.TestUtils.setMock(new dfsle.ESignatureAPIMock()); - - DocusignEnvelopeRequest req = new DocusignEnvelopeRequest(); - req.templateIds = new List{'01234567-abcd-ef01-2345-6789abcdef01'}; - req.recordId = '001000000ABC123'; - req.emailSubject = 'Custom: Please review and sign'; - - // Act - Test.startTest(); - List results = - DocusignCompositeEnvelopeBuilder.sendCompositeEnvelope( - new List{req} - ); - Test.stopTest(); - - // Assert - System.assertEquals(true, results[0].success, 'Should succeed with custom subject'); - } - - @isTest - static void testWithoutEmailSubject() { - // Arrange - dfsle.TestUtils.setMock(new dfsle.ESignatureAPIMock()); - - DocusignEnvelopeRequest req = new DocusignEnvelopeRequest(); - req.templateIds = new List{'01234567-abcd-ef01-2345-6789abcdef01'}; - req.recordId = '001000000ABC123'; - req.emailSubject = null; - - // Act - Test.startTest(); - List results = - DocusignCompositeEnvelopeBuilder.sendCompositeEnvelope( - new List{req} - ); - Test.stopTest(); - // Assert System.assertEquals(true, results[0].success, 'Should succeed without subject'); } -} + + @isTest + static void testEmailSubjectTruncation() { + // Arrange + dfsle.TestUtils.setMock(new dfsle.ESignatureAPIMock()); + + DocusignEnvelopeRequest req = new DocusignEnvelopeRequest(); + req.templateIds = new List{'01234567-abcd-ef01-2345-6789abcdef01'}; + req.recordId = '001000000ABC123'; + // Create a subject longer than 100 characters + req.emailSubject = 'This is a very long email subject that exceeds the one hundred character limit imposed by Docusign API requirements and should be truncated appropriately to prevent errors during envelope creation.'; + System.assert(req.emailSubject.length() > 100, 'Test setup: subject should be > 100 chars'); + + // Act + Test.startTest(); + List results = + DocusignCompositeEnvelopeBuilder.sendCompositeEnvelope( + new List{req} + ); + Test.stopTest(); + + // Assert + System.assertEquals(true, results[0].success, 'Should succeed with truncated subject'); + // Note: We can't easily test the actual truncation in this mock-based test, + // but the code change ensures truncation happens before the API call + } +} \ No newline at end of file