public with sharing class DocusignESignatureService { public class ESignatureAccountConfig { @AuraEnabled public String accountCode; @AuraEnabled public String accountDisplayName; @AuraEnabled public String environment; @AuraEnabled public String eSignatureAuthNamedCredential; @AuraEnabled public String eSignatureRestNamedCredential; @AuraEnabled public String eSignatureAccountId; } public class ApiResponse { @AuraEnabled public Boolean success; @AuraEnabled public Integer statusCode; @AuraEnabled public String message; @AuraEnabled public String requestPath; @AuraEnabled public String responseBody; } public class ESignatureAccountSummary { @AuraEnabled public String accountId; @AuraEnabled public String accountName; @AuraEnabled public String baseUri; @AuraEnabled public Boolean isDefault; @AuraEnabled public String rawJson; } public class TemplateSummary { @AuraEnabled public String templateId; @AuraEnabled public String name; @AuraEnabled public String description; @AuraEnabled public String shared; @AuraEnabled public String lastModified; @AuraEnabled public String rawJson; } public class EnvelopeSummary { @AuraEnabled public String envelopeId; @AuraEnabled public String emailSubject; @AuraEnabled public String status; @AuraEnabled public String createdDateTime; @AuraEnabled public String sentDateTime; @AuraEnabled public String completedDateTime; @AuraEnabled public String rawJson; } @AuraEnabled(cacheable=true) public static ESignatureAccountConfig getAccountConfig(String accountCode) { CLM_Account_Setting__mdt row = requireAccountSetting(accountCode); ESignatureAccountConfig config = new ESignatureAccountConfig(); config.accountCode = String.isNotBlank(row.Account_Code__c) ? row.Account_Code__c : row.DeveloperName; config.accountDisplayName = String.isNotBlank(row.Account_Display_Name__c) ? row.Account_Display_Name__c : row.DeveloperName; config.environment = row.Environment_Code__c; config.eSignatureAuthNamedCredential = row.ESignature_Auth_Named_Credential__c; config.eSignatureRestNamedCredential = row.ESignature_Rest_Named_Credential__c; config.eSignatureAccountId = row.ESignature_Account_Id__c; return config; } @AuraEnabled(cacheable=false) public static ApiResponse probe(String accountCode, String relativePath) { CLM_Account_Setting__mdt row = requireAccountSetting(accountCode); String normalizedPath = normalizePath(relativePath); HttpRequest req = new HttpRequest(); req.setEndpoint(buildEndpoint(normalizedPath, row.ESignature_Rest_Named_Credential__c)); req.setMethod('GET'); req.setTimeout(30000); HttpResponse res = new Http().send(req); ApiResponse response = new ApiResponse(); response.success = res.getStatusCode() >= 200 && res.getStatusCode() < 300; response.statusCode = res.getStatusCode(); response.message = response.success ? 'eSignature request succeeded.' : 'eSignature request failed.'; response.requestPath = normalizedPath; response.responseBody = res.getBody(); return response; } @AuraEnabled(cacheable=false) public static List listAccounts(String accountCode) { ApiResponse response = getLoginInformation(accountCode); if (!response.success) { throw new AuraHandledException('eSignature API Error (HTTP ' + response.statusCode + '): ' + response.responseBody); } return parseAccountList(response.responseBody); } @AuraEnabled(cacheable=false) public static ApiResponse getLoginInformation(String accountCode) { return probe(accountCode, '/v2.1/login_information'); } @AuraEnabled(cacheable=false) public static ApiResponse getUserInfo(String accountCode) { CLM_Account_Setting__mdt row = requireAccountSetting(accountCode); HttpRequest req = new HttpRequest(); req.setEndpoint(buildEndpoint('/oauth/userinfo', authNamedCredential(row))); req.setMethod('GET'); req.setTimeout(30000); HttpResponse res = new Http().send(req); ApiResponse response = new ApiResponse(); response.success = res.getStatusCode() >= 200 && res.getStatusCode() < 300; response.statusCode = res.getStatusCode(); response.message = response.success ? 'eSignature user info request succeeded.' : 'eSignature user info request failed.'; response.requestPath = '/oauth/userinfo'; response.responseBody = res.getBody(); return response; } @AuraEnabled(cacheable=false) public static ApiResponse getAccountInformation(String accountCode, String eSignatureAccountId) { CLM_Account_Setting__mdt row = requireAccountSetting(accountCode); String targetAccountId = requireESignatureAccountId(row, accountCode, eSignatureAccountId); return probe(accountCode, '/v2.1/accounts/' + targetAccountId); } @AuraEnabled(cacheable=false) public static List listTemplates(String accountCode) { CLM_Account_Setting__mdt row = requireAccountSetting(accountCode); String targetAccountId = requireESignatureAccountId(row, accountCode, null); ApiResponse response = probe(accountCode, '/v2.1/accounts/' + targetAccountId + '/templates'); if (!response.success) { throw new AuraHandledException('eSignature API Error (HTTP ' + response.statusCode + '): ' + response.responseBody); } return parseTemplateList(response.responseBody); } @AuraEnabled(cacheable=false) public static List listEnvelopes(String accountCode, String fromDate) { CLM_Account_Setting__mdt row = requireAccountSetting(accountCode); String targetAccountId = requireESignatureAccountId(row, accountCode, null); String normalizedFromDate = String.isBlank(fromDate) ? DateTime.newInstanceGMT(Date.today().addDays(-30), Time.newInstance(0, 0, 0, 0)).formatGMT('yyyy-MM-dd') : fromDate.trim(); ApiResponse response = probe( accountCode, '/v2.1/accounts/' + targetAccountId + '/envelopes?from_date=' + EncodingUtil.urlEncode(normalizedFromDate, 'UTF-8') ); if (!response.success) { throw new AuraHandledException('eSignature API Error (HTTP ' + response.statusCode + '): ' + response.responseBody); } return parseEnvelopeList(response.responseBody); } @TestVisible private static List parseAccountList(String body) { Object root = JSON.deserializeUntyped(body); List records = new List(); if (root instanceof List) { records = (List) root; } else if (root instanceof Map) { Object loginAccounts = ((Map) root).get('loginAccounts'); if (loginAccounts instanceof List) { records = (List) loginAccounts; } Object accounts = ((Map) root).get('accounts'); if (records.isEmpty() && accounts instanceof List) { records = (List) accounts; } } List summaries = new List(); for (Object record : records) { if (!(record instanceof Map)) { continue; } Map row = (Map) record; ESignatureAccountSummary summary = new ESignatureAccountSummary(); summary.accountId = firstString(row, new List{ 'accountId', 'account_id' }); summary.accountName = firstString(row, new List{ 'accountName', 'account_name', 'name' }); summary.baseUri = firstString(row, new List{ 'baseUri', 'base_uri', 'baseUrl', 'base_url' }); summary.isDefault = parseBoolean(row.get('isDefault')); summary.rawJson = JSON.serialize(row); summaries.add(summary); } return summaries; } @TestVisible private static List parseTemplateList(String body) { List records = extractCollection(body, new List{ 'envelopeTemplates', 'templates' }); List summaries = new List(); for (Object record : records) { if (!(record instanceof Map)) { continue; } Map row = (Map) record; TemplateSummary summary = new TemplateSummary(); summary.templateId = firstString(row, new List{ 'templateId', 'template_id' }); summary.name = firstString(row, new List{ 'name' }); summary.description = firstString(row, new List{ 'description' }); summary.shared = firstString(row, new List{ 'shared' }); summary.lastModified = firstString(row, new List{ 'lastModified', 'last_modified', 'lastModifiedDateTime' }); summary.rawJson = JSON.serialize(row); summaries.add(summary); } return summaries; } @TestVisible private static List parseEnvelopeList(String body) { List records = extractCollection(body, new List{ 'envelopes' }); List summaries = new List(); for (Object record : records) { if (!(record instanceof Map)) { continue; } Map row = (Map) record; EnvelopeSummary summary = new EnvelopeSummary(); summary.envelopeId = firstString(row, new List{ 'envelopeId', 'envelope_id' }); summary.emailSubject = firstString(row, new List{ 'emailSubject', 'email_subject' }); summary.status = firstString(row, new List{ 'status' }); summary.createdDateTime = firstString(row, new List{ 'createdDateTime', 'created_datetime' }); summary.sentDateTime = firstString(row, new List{ 'sentDateTime', 'sent_datetime' }); summary.completedDateTime = firstString(row, new List{ 'completedDateTime', 'completed_datetime' }); summary.rawJson = JSON.serialize(row); summaries.add(summary); } return summaries; } @TestVisible private static String buildEndpoint(String relativePath, String namedCredential) { if (String.isBlank(namedCredential)) { throw new AuraHandledException('No eSignature named credential is configured for this account.'); } return 'callout:' + namedCredential + normalizePath(relativePath); } private static String normalizePath(String relativePath) { if (String.isBlank(relativePath)) { throw new AuraHandledException('A relative path is required.'); } return relativePath.startsWith('/') ? relativePath : '/' + relativePath; } private static CLM_Account_Setting__mdt requireAccountSetting(String accountCode) { String normalizedCode = String.isBlank(accountCode) ? null : accountCode.trim(); List rows = new List(); if (String.isNotBlank(normalizedCode)) { rows = [ SELECT DeveloperName, Account_Code__c, Account_Display_Name__c, Environment_Code__c, ESignature_Auth_Named_Credential__c, ESignature_Rest_Named_Credential__c, ESignature_Account_Id__c, Active__c FROM CLM_Account_Setting__mdt WHERE Active__c = true AND DeveloperName = :normalizedCode LIMIT 1 ]; if (rows.isEmpty()) { rows = [ SELECT DeveloperName, Account_Code__c, Account_Display_Name__c, Environment_Code__c, ESignature_Auth_Named_Credential__c, ESignature_Rest_Named_Credential__c, ESignature_Account_Id__c, Active__c FROM CLM_Account_Setting__mdt WHERE Active__c = true AND Account_Code__c = :normalizedCode LIMIT 1 ]; } } if (rows.isEmpty()) { throw new AuraHandledException('No active CLM account setting was found for ' + accountCode + '.'); } CLM_Account_Setting__mdt row = rows[0]; if (String.isBlank(row.ESignature_Rest_Named_Credential__c)) { throw new AuraHandledException('No eSignature named credential is configured for ' + accountCode + '.'); } return row; } private static String authNamedCredential(CLM_Account_Setting__mdt row) { return String.isNotBlank(row.ESignature_Auth_Named_Credential__c) ? row.ESignature_Auth_Named_Credential__c : row.ESignature_Rest_Named_Credential__c; } private static String requireESignatureAccountId(CLM_Account_Setting__mdt row, String accountCode, String eSignatureAccountId) { String targetAccountId = String.isNotBlank(eSignatureAccountId) ? eSignatureAccountId : row.ESignature_Account_Id__c; if (String.isBlank(targetAccountId)) { throw new AuraHandledException('No eSignature account id is configured for ' + accountCode + '.'); } return targetAccountId; } private static List extractCollection(String body, List keys) { Object root = JSON.deserializeUntyped(body); if (root instanceof List) { return (List) root; } if (root instanceof Map) { Map source = (Map) root; for (String key : keys) { Object records = source.get(key); if (records instanceof List) { return (List) records; } } } return new List(); } private static String firstString(Map source, List keys) { for (String key : keys) { Object value = source.get(key); if (value != null) { String text = String.valueOf(value); if (String.isNotBlank(text)) { return text; } } } return null; } private static Boolean parseBoolean(Object value) { if (value == null) { return false; } if (value instanceof Boolean) { return (Boolean) value; } return String.valueOf(value).toLowerCase() == 'true'; } }