/** * @description Manages Docusign API credentials and access tokens * @author Paul Huliganga * @date 2026-02-23 */ public with sharing class DocusignCredentials { private String baseUrl; private String accountId; private String accessToken; private DateTime tokenExpiry; // Singleton instance private static DocusignCredentials instance; // Flag to skip loadCredentials for setTestCredentials private static Boolean skipLoadForNextInstance = false; /** * @description Private constructor (singleton pattern) */ private DocusignCredentials() { // Only load credentials if not explicitly skipped (for setTestCredentials) if (!skipLoadForNextInstance) { loadCredentials(); } skipLoadForNextInstance = false; // Reset flag } /** * @description Gets singleton instance of credentials * @return DocusignCredentials instance */ public static DocusignCredentials getInstance() { if (instance == null) { instance = new DocusignCredentials(); } // Refresh token if expired if (instance.isTokenExpired()) { instance.refreshAccessToken(); } return instance; } /** * @description Loads credentials from Named Credential or Custom Settings */ private void loadCredentials() { // Option 1: Using Named Credential (preferred) // When using Named Credential, the platform handles authentication automatically // We just need the account ID and base URL try { // Try to load from Custom Settings first Docusign_Configuration__c config = Docusign_Configuration__c.getOrgDefaults(); if (config != null && String.isNotBlank(config.Account_Id__c)) { this.accountId = config.Account_Id__c; this.baseUrl = String.isNotBlank(config.Base_URL__c) ? config.Base_URL__c : 'callout:DocusignAPI'; // Default to Named Credential // If using Named Credential, token is managed by platform if (this.baseUrl.startsWith('callout:')) { this.accessToken = 'MANAGED_BY_NAMED_CREDENTIAL'; this.tokenExpiry = DateTime.now().addHours(1); } else { // Manual token management would go here // For now, throw exception - must configure Named Credential throw new CredentialException('Manual token management not implemented. Please use Named Credential.'); } } else { throw new CredentialException('Docusign credentials not configured. Please set up Custom Settings: Docusign_Configuration__c'); } } catch (Exception e) { throw new CredentialException('Failed to load Docusign credentials: ' + e.getMessage()); } } /** * @description Checks if access token is expired * @return True if token is expired or about to expire (within 5 minutes) */ private Boolean isTokenExpired() { if (this.tokenExpiry == null) { return true; } // Refresh 5 minutes before expiry to avoid edge cases DateTime threshold = DateTime.now().addMinutes(5); return this.tokenExpiry < threshold; } /** * @description Refreshes access token (JWT or OAuth2) * Note: In production with Named Credential, this is handled automatically by Salesforce */ private void refreshAccessToken() { // If using Named Credential, no manual refresh needed if (this.baseUrl.startsWith('callout:')) { this.tokenExpiry = DateTime.now().addHours(1); return; } // Manual JWT token refresh would go here // This is a placeholder for future enhancement throw new CredentialException('Manual token refresh not implemented. Use Named Credential for automatic token management.'); } /** * @description Gets access token * @return Access token string */ public String getAccessToken() { if (isTokenExpired()) { refreshAccessToken(); } return this.accessToken; } /** * @description Gets Docusign account ID * @return Account ID (GUID) */ public String getAccountId() { return this.accountId; } /** * @description Gets base URL for Docusign API * @return Base URL (either Named Credential callout or full URL) */ public String getBaseUrl() { return this.baseUrl; } /** * @description Validates that credentials are properly configured * @return True if valid * @throws CredentialException if invalid */ public Boolean validate() { if (String.isBlank(this.accountId)) { throw new CredentialException('Docusign Account ID is not configured'); } if (String.isBlank(this.baseUrl)) { throw new CredentialException('Docusign Base URL is not configured'); } if (String.isBlank(this.accessToken)) { throw new CredentialException('Docusign Access Token is not available'); } return true; } /** * @description Resets singleton instance (for testing) */ @TestVisible private static void resetInstance() { instance = null; } /** * @description Sets credentials manually (for testing) */ @TestVisible private static void setTestCredentials(String testAccountId, String testBaseUrl, String testAccessToken) { // Set flag to skip loadCredentials for this instance skipLoadForNextInstance = true; instance = new DocusignCredentials(); // Override with specific test values instance.accountId = testAccountId; instance.baseUrl = testBaseUrl; instance.accessToken = testAccessToken; instance.tokenExpiry = DateTime.now().addHours(1); } /** * @description Custom exception for credential errors */ public class CredentialException extends Exception {} }