Compare commits
1 Commits
main
...
feature/sm
| Author | SHA1 | Date |
|---|---|---|
|
|
ac6de33317 |
|
|
@ -29,6 +29,17 @@ global with sharing class DocusignCompositeEnvelopeBuilder {
|
|||
@TestVisible
|
||||
private static final String MULTI_COPY_TEMPLATE_NAME = 'Authorization to Release Information';
|
||||
|
||||
// ============================================================
|
||||
// SMS DELIVERY: Placeholder email used when the primary recipient
|
||||
// (Docusign Recipient #1) has no email and SMS delivery is requested.
|
||||
// Docusign requires an email on every recipient even when delivery
|
||||
// is via SMS; this constant satisfies that requirement without
|
||||
// routing any actual email. Update this value if your org uses a
|
||||
// different placeholder address.
|
||||
// ============================================================
|
||||
@TestVisible
|
||||
private static final String SMS_FALLBACK_EMAIL = 'placeholder_email@docusign.com';
|
||||
|
||||
@InvocableMethod(
|
||||
label='Send Composite Docusign Envelope'
|
||||
description='Combines multiple Docusign templates into a single envelope using dfsle Apex Toolkit'
|
||||
|
|
@ -188,7 +199,7 @@ global with sharing class DocusignCompositeEnvelopeBuilder {
|
|||
}
|
||||
|
||||
// Resolve recipients from Client_Case__c lookup fields
|
||||
List<dfsle.Recipient> recipients = resolveRecipients(req.recordId);
|
||||
List<dfsle.Recipient> recipients = resolveRecipients(req.recordId, req.recipientSmsPhone);
|
||||
myEnvelope = myEnvelope.withRecipients(recipients);
|
||||
|
||||
// Set envelope subject to combined display names (deduplicated, with copy counts).
|
||||
|
|
@ -222,11 +233,25 @@ global with sharing class DocusignCompositeEnvelopeBuilder {
|
|||
String envelopeBody = bodyParts.isEmpty() ? '' : String.join(bodyParts, '\n\n');
|
||||
myEnvelope = myEnvelope.withEmail(envelopeSubject, envelopeBody);
|
||||
|
||||
// Send the envelope
|
||||
myEnvelope = dfsle.EnvelopeService.sendEnvelope(myEnvelope, true);
|
||||
|
||||
// Success
|
||||
result.envelopeId = String.valueOf(myEnvelope.docuSignId);
|
||||
// Send the envelope.
|
||||
// When a recipient SMS phone is supplied we bypass the dfsle Toolkit entirely
|
||||
// because it cannot set additionalNotifications for SMS delivery.
|
||||
// DocusignSmsEnvelopeService posts directly to the Docusign REST API instead.
|
||||
if (String.isNotBlank(req.recipientSmsPhone)) {
|
||||
String envelopeId = DocusignSmsEnvelopeService.sendEnvelope(
|
||||
req.recordId,
|
||||
sortedIds,
|
||||
docNames,
|
||||
displayNames,
|
||||
envelopeSubject,
|
||||
envelopeBody,
|
||||
req.recipientSmsPhone
|
||||
);
|
||||
result.envelopeId = envelopeId;
|
||||
} else {
|
||||
myEnvelope = dfsle.EnvelopeService.sendEnvelope(myEnvelope, true);
|
||||
result.envelopeId = String.valueOf(myEnvelope.docuSignId);
|
||||
}
|
||||
result.success = true;
|
||||
result.errorMessage = null;
|
||||
|
||||
|
|
@ -256,9 +281,13 @@ global with sharing class DocusignCompositeEnvelopeBuilder {
|
|||
* @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
|
||||
* @param smsPhone Optional SMS phone for the primary recipient when they have no email.
|
||||
* When non-blank, buildRecipient will substitute SMS_FALLBACK_EMAIL
|
||||
* for the Docusign Recipient #1 role instead of throwing an error.
|
||||
* (Only relevant for the dfsle path — the SMS service resolves its own recipients.)
|
||||
* @return List of dfsle.Recipient objects with role mappings
|
||||
*/
|
||||
private static List<dfsle.Recipient> resolveRecipients(String recordId) {
|
||||
private static List<dfsle.Recipient> resolveRecipients(String recordId, String smsPhone) {
|
||||
// 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, '
|
||||
|
|
@ -274,13 +303,13 @@ global with sharing class DocusignCompositeEnvelopeBuilder {
|
|||
// Recipient 1: Service Coordinator
|
||||
Id serviceCoordinatorId = (Id) caseRecord.get(FIELD_SERVICE_COORDINATOR);
|
||||
if (serviceCoordinatorId != null) {
|
||||
recipients.add(buildRecipient(serviceCoordinatorId, ROLE_SERVICE_COORDINATOR, routingOrder++, recordId));
|
||||
recipients.add(buildRecipient(serviceCoordinatorId, ROLE_SERVICE_COORDINATOR, routingOrder++, recordId, null));
|
||||
}
|
||||
|
||||
// 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));
|
||||
recipients.add(buildRecipient(docusignRecipientId, ROLE_DOCUSIGN_RECIPIENT, routingOrder++, recordId, smsPhone));
|
||||
}
|
||||
|
||||
if (recipients.isEmpty()) {
|
||||
|
|
@ -297,9 +326,14 @@ global with sharing class DocusignCompositeEnvelopeBuilder {
|
|||
* @param roleName The Docusign template role name
|
||||
* @param routingOrder Signing order
|
||||
* @param sourceRecordId The source Client_Case__c record ID
|
||||
* @param smsPhone Optional SMS phone number for the Docusign Recipient #1 role.
|
||||
* When non-blank and the recipient has no email, SMS_FALLBACK_EMAIL
|
||||
* is substituted so the dfsle Toolkit call can proceed.
|
||||
* (The actual SMS delivery notification is handled separately by
|
||||
* DocusignSmsEnvelopeService — this path is a safety fallback only.)
|
||||
* @return dfsle.Recipient configured for the role
|
||||
*/
|
||||
private static dfsle.Recipient buildRecipient(Id recipientId, String roleName, Integer routingOrder, String sourceRecordId) {
|
||||
private static dfsle.Recipient buildRecipient(Id recipientId, String roleName, Integer routingOrder, String sourceRecordId, String smsPhone) {
|
||||
// Determine if this is a Contact or User
|
||||
String objectType = recipientId.getSObjectType().getDescribe().getName();
|
||||
|
||||
|
|
@ -320,8 +354,14 @@ global with sharing class DocusignCompositeEnvelopeBuilder {
|
|||
}
|
||||
|
||||
if (String.isBlank(recipientEmail)) {
|
||||
throw new IllegalArgumentException('No email found for ' + roleName + ' (' + recipientName + '). '
|
||||
+ 'Please ensure the recipient has a valid email address.');
|
||||
if (roleName == ROLE_DOCUSIGN_RECIPIENT && String.isNotBlank(smsPhone)) {
|
||||
// Recipient has no email but SMS delivery is requested — substitute
|
||||
// the placeholder email so the dfsle Toolkit call does not throw.
|
||||
recipientEmail = SMS_FALLBACK_EMAIL;
|
||||
} else {
|
||||
throw new IllegalArgumentException('No email found for ' + roleName + ' (' + recipientName + '). '
|
||||
+ 'Please ensure the recipient has a valid email address.');
|
||||
}
|
||||
}
|
||||
|
||||
return dfsle.Recipient.fromSource(
|
||||
|
|
|
|||
|
|
@ -39,4 +39,11 @@ global class DocusignEnvelopeRequest {
|
|||
required=false
|
||||
)
|
||||
global Integer authReleaseFormCopies;
|
||||
|
||||
@InvocableVariable(
|
||||
label='Recipient SMS Phone'
|
||||
description='Mobile phone number for SMS delivery when the primary recipient (Docusign Recipient #1) has no email address. E.164 format preferred (e.g. +15551234567). When provided, the envelope is sent via direct Docusign REST API instead of the dfsle Toolkit so that additionalNotifications/SMS delivery can be set.'
|
||||
required=false
|
||||
)
|
||||
global String recipientSmsPhone;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,414 @@
|
|||
/**
|
||||
* @description Sends a Docusign composite envelope via direct REST API call when the
|
||||
* primary recipient (Docusign Recipient #1) does not have an email address
|
||||
* and requires SMS delivery instead.
|
||||
*
|
||||
* The dfsle Apex Toolkit does not support the Docusign
|
||||
* additionalNotifications/secondaryDeliveryMethod property needed for
|
||||
* SMS-only recipients, so this service bypasses the toolkit and calls
|
||||
* the Docusign REST API directly.
|
||||
*
|
||||
* Prerequisites:
|
||||
* - Named Credential "DocusignAPI" must be configured in the org.
|
||||
* - Docusign account must have SMS delivery enabled.
|
||||
* - Docusign_Configuration__c custom setting must have Account_Id__c populated.
|
||||
*
|
||||
* @author Paul Huliganga
|
||||
* @date 2026-02-25
|
||||
*/
|
||||
global with sharing class DocusignSmsEnvelopeService {
|
||||
|
||||
// Named Credential for Docusign REST API callouts
|
||||
@TestVisible
|
||||
private static final String NAMED_CREDENTIAL = 'callout:DocusignAPI';
|
||||
|
||||
// Endpoint path template — {0} is replaced with the accountId
|
||||
@TestVisible
|
||||
private static final String ENVELOPES_PATH = '/accounts/{0}/envelopes';
|
||||
|
||||
// Dummy email used for SMS-only recipients. Docusign requires an email address
|
||||
// on every recipient even when delivery is via SMS. This placeholder satisfies
|
||||
// that requirement without routing any actual email.
|
||||
@TestVisible
|
||||
private static final String SMS_PLACEHOLDER_EMAIL = 'placeholder_email@docusign.com';
|
||||
|
||||
// Country code assumed when the caller supplies a bare 10-digit US number.
|
||||
// The flow enforces E.164 format (e.g. +15551234567) so this is a safety fallback.
|
||||
private static final String DEFAULT_COUNTRY_CODE = '1';
|
||||
|
||||
/**
|
||||
* @description Builds and sends a composite Docusign envelope with SMS delivery for
|
||||
* the primary recipient. Mirrors the logic in DocusignCompositeEnvelopeBuilder
|
||||
* but constructs the JSON payload manually so that additionalNotifications
|
||||
* can be included on the recipient object.
|
||||
*
|
||||
* @param recordId The Client_Case__c record ID (used to look up recipients)
|
||||
* @param sortedIds Template IDs in sorted order (may contain duplicates for multi-copy)
|
||||
* @param docNames Document labels in same order as sortedIds
|
||||
* @param displayNames Deduplicated display labels for email subject / body
|
||||
* @param envelopeSubject Email subject line (already formatted and truncated)
|
||||
* @param envelopeBody Email body text (already formatted and deduplicated)
|
||||
* @param smsPhone Mobile phone number for SMS delivery (E.164 preferred, e.g. +15551234567)
|
||||
* @return Docusign envelope ID string on success
|
||||
* @throws IllegalArgumentException when recipients or configuration data are missing
|
||||
* @throws CalloutException on HTTP errors
|
||||
*/
|
||||
global static String sendEnvelope(
|
||||
String recordId,
|
||||
List<String> sortedIds,
|
||||
List<String> docNames,
|
||||
List<String> displayNames,
|
||||
String envelopeSubject,
|
||||
String envelopeBody,
|
||||
String smsPhone
|
||||
) {
|
||||
// ─── Fetch Docusign account ID from custom setting ───────────────────────────
|
||||
Docusign_Configuration__c config = Docusign_Configuration__c.getInstance();
|
||||
if (config == null || String.isBlank(config.Account_Id__c)) {
|
||||
throw new IllegalArgumentException(
|
||||
'Docusign_Configuration__c is not configured. '
|
||||
+ 'Please set Account_Id__c in the Docusign Configuration custom setting.'
|
||||
);
|
||||
}
|
||||
String accountId = config.Account_Id__c;
|
||||
|
||||
// ─── Resolve recipients from Client_Case__c ───────────────────────────────────
|
||||
RecipientInfo serviceCoordinator = null;
|
||||
RecipientInfo docusignRecipient = null;
|
||||
|
||||
String query = 'SELECT Id, Service_Coordinator__c, Docusign_Recipient_1__c '
|
||||
+ 'FROM Client_Case__c WHERE Id = :recordId LIMIT 1';
|
||||
Client_Case__c caseRecord = Database.query(query);
|
||||
|
||||
Id scId = (Id) caseRecord.get('Service_Coordinator__c');
|
||||
Id drId = (Id) caseRecord.get('Docusign_Recipient_1__c');
|
||||
|
||||
if (scId != null) {
|
||||
serviceCoordinator = resolveRecipient(scId, 'Service Coordinator', 1, null, false);
|
||||
}
|
||||
if (drId != null) {
|
||||
// SMS phone provided — substitute placeholder email for the Docusign Recipient
|
||||
docusignRecipient = resolveRecipient(drId, 'Docusign Recipient #1', 2, smsPhone, true);
|
||||
}
|
||||
|
||||
if (serviceCoordinator == null && docusignRecipient == null) {
|
||||
throw new IllegalArgumentException(
|
||||
'No recipients found on the Client Case record. '
|
||||
+ 'Please ensure Service Coordinator and Docusign Recipient #1 are populated.'
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Build JSON body ─────────────────────────────────────────────────────────
|
||||
String jsonBody = buildEnvelopeJson(
|
||||
sortedIds,
|
||||
docNames,
|
||||
envelopeSubject,
|
||||
envelopeBody,
|
||||
serviceCoordinator,
|
||||
docusignRecipient,
|
||||
recordId
|
||||
);
|
||||
|
||||
// ─── Call the Docusign REST API ───────────────────────────────────────────────
|
||||
String endpoint = NAMED_CREDENTIAL
|
||||
+ String.format(ENVELOPES_PATH, new List<Object>{ accountId });
|
||||
|
||||
HttpRequest httpReq = new HttpRequest();
|
||||
httpReq.setEndpoint(endpoint);
|
||||
httpReq.setMethod('POST');
|
||||
httpReq.setHeader('Content-Type', 'application/json');
|
||||
httpReq.setHeader('Accept', 'application/json');
|
||||
httpReq.setBody(jsonBody);
|
||||
|
||||
Http http = new Http();
|
||||
HttpResponse httpResp = http.send(httpReq);
|
||||
|
||||
Integer statusCode = httpResp.getStatusCode();
|
||||
String responseBody = httpResp.getBody();
|
||||
|
||||
if (statusCode == 200 || statusCode == 201) {
|
||||
// Parse envelopeId from response
|
||||
Map<String, Object> responseMap = (Map<String, Object>) JSON.deserializeUntyped(responseBody);
|
||||
Object envelopeIdObj = responseMap.get('envelopeId');
|
||||
if (envelopeIdObj == null) {
|
||||
throw new CalloutException(
|
||||
'Docusign API returned success but no envelopeId in response: ' + responseBody
|
||||
);
|
||||
}
|
||||
return String.valueOf(envelopeIdObj);
|
||||
} else {
|
||||
// Try to extract a meaningful error message from the response JSON
|
||||
String errorDetail = parseDocusignError(responseBody);
|
||||
throw new CalloutException(
|
||||
'Docusign API returned HTTP ' + statusCode + ': ' + errorDetail
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────────
|
||||
// PRIVATE HELPERS
|
||||
// ─────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* @description Resolves a recipient's name and email from a Contact or User record.
|
||||
* When applySmsPlaceholder is true and the record has no email,
|
||||
* the SMS_PLACEHOLDER_EMAIL constant is used instead.
|
||||
*/
|
||||
private static RecipientInfo resolveRecipient(
|
||||
Id recipientId,
|
||||
String roleName,
|
||||
Integer routingOrder,
|
||||
String smsPhone,
|
||||
Boolean applySmsPlaceholder
|
||||
) {
|
||||
String objectType = recipientId.getSObjectType().getDescribe().getName();
|
||||
String name;
|
||||
String email;
|
||||
|
||||
if (objectType == 'Contact') {
|
||||
Contact c = [SELECT Id, Name, Email FROM Contact WHERE Id = :recipientId LIMIT 1];
|
||||
name = c.Name;
|
||||
email = c.Email;
|
||||
} else if (objectType == 'User') {
|
||||
User u = [SELECT Id, Name, Email FROM User WHERE Id = :recipientId LIMIT 1];
|
||||
name = u.Name;
|
||||
email = u.Email;
|
||||
} else {
|
||||
throw new IllegalArgumentException(
|
||||
'Unsupported recipient type: ' + objectType + '. Expected Contact or User.'
|
||||
);
|
||||
}
|
||||
|
||||
if (String.isBlank(email)) {
|
||||
if (applySmsPlaceholder) {
|
||||
email = SMS_PLACEHOLDER_EMAIL;
|
||||
} else {
|
||||
throw new IllegalArgumentException(
|
||||
'No email found for ' + roleName + ' (' + name + '). '
|
||||
+ 'Please ensure the recipient has a valid email address.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
RecipientInfo info = new RecipientInfo();
|
||||
info.name = name;
|
||||
info.email = email;
|
||||
info.roleName = roleName;
|
||||
info.routingOrder = routingOrder;
|
||||
info.smsPhone = applySmsPlaceholder ? smsPhone : null;
|
||||
return info;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Constructs the full envelope JSON payload for the Docusign REST API.
|
||||
* Uses compositeTemplates so that all selected templates are merged into
|
||||
* a single envelope, matching what the dfsle Toolkit does internally.
|
||||
*
|
||||
* Docusign compositeTemplates reference:
|
||||
* https://developers.docusign.com/docs/esign-rest-api/reference/envelopes/envelopes/create/
|
||||
*/
|
||||
@TestVisible
|
||||
private static String buildEnvelopeJson(
|
||||
List<String> sortedIds,
|
||||
List<String> docNames,
|
||||
String envelopeSubject,
|
||||
String envelopeBody,
|
||||
RecipientInfo serviceCoordinator,
|
||||
RecipientInfo docusignRecipient,
|
||||
String sourceRecordId
|
||||
) {
|
||||
// Build the recipients object (shared across all compositeTemplates)
|
||||
// Each recipient gets a unique recipientId (sequential string integer).
|
||||
List<Object> signers = new List<Object>();
|
||||
Integer recipientIdCounter = 1;
|
||||
|
||||
if (serviceCoordinator != null) {
|
||||
signers.add(buildSignerMap(serviceCoordinator, String.valueOf(recipientIdCounter++)));
|
||||
}
|
||||
if (docusignRecipient != null) {
|
||||
signers.add(buildSignerMap(docusignRecipient, String.valueOf(recipientIdCounter++)));
|
||||
}
|
||||
|
||||
Map<String, Object> recipientsMap = new Map<String, Object>{
|
||||
'signers' => signers
|
||||
};
|
||||
|
||||
// Build compositeTemplates array — one entry per document (template ID + label)
|
||||
List<Object> compositeTemplates = new List<Object>();
|
||||
for (Integer i = 0; i < sortedIds.size(); i++) {
|
||||
String templateId = sortedIds[i];
|
||||
String docLabel = docNames[i];
|
||||
|
||||
Map<String, Object> serverTemplate = new Map<String, Object>{
|
||||
'sequence' => '1',
|
||||
'templateId' => templateId
|
||||
};
|
||||
|
||||
// Inline template overrides — set the document label and wire up role assignments
|
||||
List<Object> inlineRecipientSigners = new List<Object>();
|
||||
Integer irCounter = 1;
|
||||
if (serviceCoordinator != null) {
|
||||
inlineRecipientSigners.add(new Map<String, Object>{
|
||||
'recipientId' => String.valueOf(irCounter++),
|
||||
'roleName' => serviceCoordinator.roleName,
|
||||
'name' => serviceCoordinator.name,
|
||||
'email' => serviceCoordinator.email,
|
||||
'routingOrder' => String.valueOf(serviceCoordinator.routingOrder)
|
||||
});
|
||||
}
|
||||
if (docusignRecipient != null) {
|
||||
Map<String, Object> drSigner = new Map<String, Object>{
|
||||
'recipientId' => String.valueOf(irCounter++),
|
||||
'roleName' => docusignRecipient.roleName,
|
||||
'name' => docusignRecipient.name,
|
||||
'email' => docusignRecipient.email,
|
||||
'routingOrder' => String.valueOf(docusignRecipient.routingOrder)
|
||||
};
|
||||
// Add SMS additionalNotifications for this recipient
|
||||
if (String.isNotBlank(docusignRecipient.smsPhone)) {
|
||||
String[] phoneParts = parsePhone(docusignRecipient.smsPhone);
|
||||
drSigner.put('additionalNotifications', new List<Object>{
|
||||
new Map<String, Object>{
|
||||
'secondaryDeliveryMethod' => 'SMS',
|
||||
'phoneNumber' => new Map<String, Object>{
|
||||
'countryCode' => phoneParts[0],
|
||||
'number' => phoneParts[1]
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
inlineRecipientSigners.add(drSigner);
|
||||
}
|
||||
|
||||
Map<String, Object> inlineTemplate = new Map<String, Object>{
|
||||
'sequence' => '2',
|
||||
'recipients' => new Map<String, Object>{
|
||||
'signers' => inlineRecipientSigners
|
||||
},
|
||||
'document' => new Map<String, Object>{
|
||||
'name' => docLabel,
|
||||
'documentId' => String.valueOf(i + 1),
|
||||
'transformPdfFields' => 'true'
|
||||
}
|
||||
};
|
||||
|
||||
compositeTemplates.add(new Map<String, Object>{
|
||||
'serverTemplates' => new List<Object>{ serverTemplate },
|
||||
'inlineTemplates' => new List<Object>{ inlineTemplate },
|
||||
'recipients' => recipientsMap
|
||||
});
|
||||
}
|
||||
|
||||
Map<String, Object> envelopeDefinition = new Map<String, Object>{
|
||||
'status' => 'sent',
|
||||
'emailSubject' => envelopeSubject,
|
||||
'emailBlurb' => envelopeBody,
|
||||
'compositeTemplates' => compositeTemplates
|
||||
};
|
||||
|
||||
return JSON.serialize(envelopeDefinition);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Builds the signer map for a recipient in the envelope-level recipients object.
|
||||
* Adds additionalNotifications for SMS recipients.
|
||||
*/
|
||||
private static Map<String, Object> buildSignerMap(RecipientInfo info, String recipientId) {
|
||||
Map<String, Object> signer = new Map<String, Object>{
|
||||
'recipientId' => recipientId,
|
||||
'name' => info.name,
|
||||
'email' => info.email,
|
||||
'roleName' => info.roleName,
|
||||
'routingOrder' => String.valueOf(info.routingOrder)
|
||||
};
|
||||
|
||||
if (String.isNotBlank(info.smsPhone)) {
|
||||
String[] phoneParts = parsePhone(info.smsPhone);
|
||||
signer.put('additionalNotifications', new List<Object>{
|
||||
new Map<String, Object>{
|
||||
'secondaryDeliveryMethod' => 'SMS',
|
||||
'phoneNumber' => new Map<String, Object>{
|
||||
'countryCode' => phoneParts[0],
|
||||
'number' => phoneParts[1]
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return signer;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Parses a phone number into [countryCode, nationalNumber].
|
||||
* Accepts E.164 format (+15551234567) or a bare 10-digit number.
|
||||
* Returns [DEFAULT_COUNTRY_CODE, strippedNumber] for bare numbers.
|
||||
*
|
||||
* @param phone Raw phone string from the flow input
|
||||
* @return String array: index 0 = country code, index 1 = national number (digits only)
|
||||
*/
|
||||
@TestVisible
|
||||
private static String[] parsePhone(String phone) {
|
||||
if (String.isBlank(phone)) {
|
||||
return new String[]{ DEFAULT_COUNTRY_CODE, '' };
|
||||
}
|
||||
|
||||
String stripped = phone.replaceAll('[^\\d+]', ''); // keep digits and leading +
|
||||
|
||||
if (stripped.startsWith('+')) {
|
||||
// E.164: determine country code length heuristically
|
||||
// We only support 1-digit (+1) and 2-digit (+XX) country codes for now
|
||||
String digits = stripped.substring(1); // remove leading +
|
||||
if (digits.length() == 11 && digits.startsWith('1')) {
|
||||
// +1XXXXXXXXXX (North American Numbering Plan)
|
||||
return new String[]{ '1', digits.substring(1) };
|
||||
} else if (digits.length() == 12) {
|
||||
// +XXXXXXXXXXXX (2-digit country code + 10 digit number)
|
||||
return new String[]{ digits.left(2), digits.substring(2) };
|
||||
} else {
|
||||
// Fall back: treat first digit(s) as country code not supported; use full digits
|
||||
return new String[]{ DEFAULT_COUNTRY_CODE, digits };
|
||||
}
|
||||
}
|
||||
|
||||
// Bare digits — assume DEFAULT_COUNTRY_CODE
|
||||
return new String[]{ DEFAULT_COUNTRY_CODE, stripped };
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Attempts to extract a human-readable error message from a Docusign
|
||||
* REST API error response JSON.
|
||||
* @param responseBody Raw HTTP response body
|
||||
* @return Error string with errorCode and message if available, otherwise raw body
|
||||
*/
|
||||
private static String parseDocusignError(String responseBody) {
|
||||
if (String.isBlank(responseBody)) {
|
||||
return '(no response body)';
|
||||
}
|
||||
try {
|
||||
Map<String, Object> errorMap = (Map<String, Object>) JSON.deserializeUntyped(responseBody);
|
||||
String errorCode = errorMap.containsKey('errorCode') ? String.valueOf(errorMap.get('errorCode')) : null;
|
||||
String message = errorMap.containsKey('message') ? String.valueOf(errorMap.get('message')) : null;
|
||||
if (String.isNotBlank(errorCode) || String.isNotBlank(message)) {
|
||||
return (errorCode != null ? '[' + errorCode + '] ' : '') + (message != null ? message : '');
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
// Non-JSON response — return raw body below
|
||||
}
|
||||
return responseBody.left(500); // cap at 500 chars to avoid log spam
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────────
|
||||
// INNER CLASSES
|
||||
// ─────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* @description Lightweight holder for resolved recipient data.
|
||||
*/
|
||||
private class RecipientInfo {
|
||||
String name;
|
||||
String email;
|
||||
String roleName;
|
||||
Integer routingOrder;
|
||||
String smsPhone; // null for email recipients; phone string for SMS recipients
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
|
||||
<apiVersion>60.0</apiVersion>
|
||||
<status>Active</status>
|
||||
</ApexClass>
|
||||
|
|
@ -35,6 +35,12 @@
|
|||
<elementReference>authReleaseFormCopies</elementReference>
|
||||
</value>
|
||||
</inputParameters>
|
||||
<inputParameters>
|
||||
<name>recipientSmsPhone</name>
|
||||
<value>
|
||||
<elementReference>recipientSmsPhone</elementReference>
|
||||
</value>
|
||||
</inputParameters>
|
||||
<nameSegment>DocusignCompositeEnvelopeBuilder</nameSegment>
|
||||
<offset>0</offset>
|
||||
<outputParameters>
|
||||
|
|
@ -179,7 +185,7 @@
|
|||
<name>Is_Language_Selected</name>
|
||||
<label>Is Language Selected?</label>
|
||||
<locationX>611</locationX>
|
||||
<locationY>242</locationY>
|
||||
<locationY>458</locationY>
|
||||
<defaultConnector>
|
||||
<targetReference>Language_Not_Added_Screen</targetReference>
|
||||
</defaultConnector>
|
||||
|
|
@ -225,6 +231,109 @@
|
|||
<label>Yes</label>
|
||||
</rules>
|
||||
</decisions>
|
||||
<recordLookups>
|
||||
<name>Get_Recipient_Contact</name>
|
||||
<label>Get Recipient Contact</label>
|
||||
<locationX>611</locationX>
|
||||
<locationY>242</locationY>
|
||||
<assignNullValuesIfNoRecordsFound>true</assignNullValuesIfNoRecordsFound>
|
||||
<connector>
|
||||
<targetReference>Is_Recipient_Email_Blank</targetReference>
|
||||
</connector>
|
||||
<filterLogic>and</filterLogic>
|
||||
<filters>
|
||||
<field>Id</field>
|
||||
<operator>EqualTo</operator>
|
||||
<value>
|
||||
<elementReference>Get_Records.Docusign_Recipient_1__c</elementReference>
|
||||
</value>
|
||||
</filters>
|
||||
<getFirstRecordOnly>true</getFirstRecordOnly>
|
||||
<object>Contact</object>
|
||||
<queriedFields>Id</queriedFields>
|
||||
<queriedFields>Email</queriedFields>
|
||||
<storeOutputAutomatically>true</storeOutputAutomatically>
|
||||
</recordLookups>
|
||||
<decisions>
|
||||
<name>Is_Recipient_Email_Blank</name>
|
||||
<label>Is Recipient Email Blank?</label>
|
||||
<locationX>611</locationX>
|
||||
<locationY>350</locationY>
|
||||
<defaultConnector>
|
||||
<targetReference>Is_Language_Selected</targetReference>
|
||||
</defaultConnector>
|
||||
<defaultConnectorLabel>Has Email - Continue</defaultConnectorLabel>
|
||||
<rules>
|
||||
<name>Recipient_Has_No_Email</name>
|
||||
<conditionLogic>or</conditionLogic>
|
||||
<conditions>
|
||||
<leftValueReference>Get_Recipient_Contact.Email</leftValueReference>
|
||||
<operator>IsNull</operator>
|
||||
<rightValue>
|
||||
<booleanValue>true</booleanValue>
|
||||
</rightValue>
|
||||
</conditions>
|
||||
<conditions>
|
||||
<leftValueReference>Get_Recipient_Contact.Email</leftValueReference>
|
||||
<operator>EqualTo</operator>
|
||||
<rightValue>
|
||||
<stringValue></stringValue>
|
||||
</rightValue>
|
||||
</conditions>
|
||||
<connector>
|
||||
<targetReference>SMS_Required_Screen</targetReference>
|
||||
</connector>
|
||||
<label>No Email - SMS Required</label>
|
||||
</rules>
|
||||
</decisions>
|
||||
<screens>
|
||||
<name>SMS_Required_Screen</name>
|
||||
<label>SMS Delivery Required</label>
|
||||
<locationX>842</locationX>
|
||||
<locationY>458</locationY>
|
||||
<allowBack>false</allowBack>
|
||||
<allowFinish>true</allowFinish>
|
||||
<allowPause>false</allowPause>
|
||||
<connector>
|
||||
<targetReference>Is_Language_Selected</targetReference>
|
||||
</connector>
|
||||
<fields>
|
||||
<name>SmsRequiredNotice</name>
|
||||
<fieldText><p>⚠️ The primary recipient <strong>({!Get_Records.Docusign_Recipient_1__c})</strong> does not have an email address on file.</p><p><br></p><p>The DocuSign envelope will be delivered via <strong>SMS text message</strong> instead. Please enter the recipient's mobile phone number below.</p><p><br></p><p>Include the country code in E.164 format, e.g. <strong>+15551234567</strong> for a US number.</p></fieldText>
|
||||
<fieldType>DisplayText</fieldType>
|
||||
<styleProperties>
|
||||
<verticalAlignment>
|
||||
<stringValue>top</stringValue>
|
||||
</verticalAlignment>
|
||||
<width>
|
||||
<stringValue>12</stringValue>
|
||||
</width>
|
||||
</styleProperties>
|
||||
</fields>
|
||||
<fields>
|
||||
<name>recipientSmsPhone_Input</name>
|
||||
<dataType>String</dataType>
|
||||
<fieldText>Mobile Phone Number</fieldText>
|
||||
<fieldType>InputField</fieldType>
|
||||
<helpText>Enter the recipient's mobile phone number in E.164 format (e.g. +15551234567). The country code and + prefix are required for international numbers.</helpText>
|
||||
<isRequired>true</isRequired>
|
||||
<outputParameters>
|
||||
<assignToReference>recipientSmsPhone</assignToReference>
|
||||
<name>value</name>
|
||||
</outputParameters>
|
||||
<styleProperties>
|
||||
<verticalAlignment>
|
||||
<stringValue>top</stringValue>
|
||||
</verticalAlignment>
|
||||
<width>
|
||||
<stringValue>6</stringValue>
|
||||
</width>
|
||||
</styleProperties>
|
||||
</fields>
|
||||
<nextOrFinishButtonLabel>Next</nextOrFinishButtonLabel>
|
||||
<showFooter>true</showFooter>
|
||||
<showHeader>true</showHeader>
|
||||
</screens>
|
||||
<environments>Default</environments>
|
||||
<interviewLabel>Docusign Envelope Templates V3 {!$Flow.CurrentDateTime}</interviewLabel>
|
||||
<label>Docusign Envelope Templates V3</label>
|
||||
|
|
@ -315,7 +424,7 @@
|
|||
<locationY>134</locationY>
|
||||
<assignNullValuesIfNoRecordsFound>false</assignNullValuesIfNoRecordsFound>
|
||||
<connector>
|
||||
<targetReference>Is_Language_Selected</targetReference>
|
||||
<targetReference>Get_Recipient_Contact</targetReference>
|
||||
</connector>
|
||||
<filterLogic>and</filterLogic>
|
||||
<filters>
|
||||
|
|
@ -329,6 +438,7 @@
|
|||
<object>Client_Case__c</object>
|
||||
<queriedFields>Id</queriedFields>
|
||||
<queriedFields>Docusign_Envelope_Language__c</queriedFields>
|
||||
<queriedFields>Docusign_Recipient_1__c</queriedFields>
|
||||
<storeOutputAutomatically>true</storeOutputAutomatically>
|
||||
</recordLookups>
|
||||
<screens>
|
||||
|
|
@ -636,6 +746,13 @@
|
|||
<booleanValue>false</booleanValue>
|
||||
</value>
|
||||
</variables>
|
||||
<variables>
|
||||
<name>recipientSmsPhone</name>
|
||||
<dataType>String</dataType>
|
||||
<isCollection>false</isCollection>
|
||||
<isInput>false</isInput>
|
||||
<isOutput>false</isOutput>
|
||||
</variables>
|
||||
<choices>
|
||||
<name>AuthCopies_1</name>
|
||||
<choiceText>1 copy</choiceText>
|
||||
|
|
|
|||
Loading…
Reference in New Issue