feat(sms): add SMS delivery for recipients without email via direct REST API
- New DocusignSmsEnvelopeService: bypasses dfsle Toolkit to POST composite envelope JSON directly to Docusign REST API; sets additionalNotifications with secondaryDeliveryMethod=SMS and phoneNumber on the primary recipient - DocusignEnvelopeRequest: new recipientSmsPhone InvocableVariable (E.164) - DocusignCompositeEnvelopeBuilder: routes to DocusignSmsEnvelopeService when recipientSmsPhone is present; adds SMS_FALLBACK_EMAIL constant; buildRecipient substitutes placeholder email for no-email primary recipients in SMS path - Flow (V3): Get_Records now fetches Docusign_Recipient_1__c; new Get_Recipient_Contact lookup checks Contact.Email; Is_Recipient_Email_Blank decision routes to SMS_Required_Screen when email is absent; phone number collected via required text input and passed as recipientSmsPhone to Apex
This commit is contained in:
parent
86e7d2fb62
commit
ac6de33317
|
|
@ -29,6 +29,17 @@ global with sharing class DocusignCompositeEnvelopeBuilder {
|
||||||
@TestVisible
|
@TestVisible
|
||||||
private static final String MULTI_COPY_TEMPLATE_NAME = 'Authorization to Release Information';
|
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(
|
@InvocableMethod(
|
||||||
label='Send Composite Docusign Envelope'
|
label='Send Composite Docusign Envelope'
|
||||||
description='Combines multiple Docusign templates into a single envelope using dfsle Apex Toolkit'
|
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
|
// 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);
|
myEnvelope = myEnvelope.withRecipients(recipients);
|
||||||
|
|
||||||
// Set envelope subject to combined display names (deduplicated, with copy counts).
|
// 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');
|
String envelopeBody = bodyParts.isEmpty() ? '' : String.join(bodyParts, '\n\n');
|
||||||
myEnvelope = myEnvelope.withEmail(envelopeSubject, envelopeBody);
|
myEnvelope = myEnvelope.withEmail(envelopeSubject, envelopeBody);
|
||||||
|
|
||||||
// Send the envelope
|
// Send the envelope.
|
||||||
myEnvelope = dfsle.EnvelopeService.sendEnvelope(myEnvelope, true);
|
// When a recipient SMS phone is supplied we bypass the dfsle Toolkit entirely
|
||||||
|
// because it cannot set additionalNotifications for SMS delivery.
|
||||||
// Success
|
// DocusignSmsEnvelopeService posts directly to the Docusign REST API instead.
|
||||||
result.envelopeId = String.valueOf(myEnvelope.docuSignId);
|
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.success = true;
|
||||||
result.errorMessage = null;
|
result.errorMessage = null;
|
||||||
|
|
||||||
|
|
@ -256,9 +281,13 @@ global with sharing class DocusignCompositeEnvelopeBuilder {
|
||||||
* @description Resolves recipients from Client_Case__c lookup fields.
|
* @description Resolves recipients from Client_Case__c lookup fields.
|
||||||
* Queries the case record and related contacts to get name/email.
|
* Queries the case record and related contacts to get name/email.
|
||||||
* @param recordId The Client_Case__c record ID
|
* @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
|
* @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
|
// Query the Client_Case__c record with recipient lookup fields
|
||||||
// NOTE: Adjust field API names if they differ in your org
|
// NOTE: Adjust field API names if they differ in your org
|
||||||
String query = 'SELECT Id, '
|
String query = 'SELECT Id, '
|
||||||
|
|
@ -274,13 +303,13 @@ global with sharing class DocusignCompositeEnvelopeBuilder {
|
||||||
// Recipient 1: Service Coordinator
|
// Recipient 1: Service Coordinator
|
||||||
Id serviceCoordinatorId = (Id) caseRecord.get(FIELD_SERVICE_COORDINATOR);
|
Id serviceCoordinatorId = (Id) caseRecord.get(FIELD_SERVICE_COORDINATOR);
|
||||||
if (serviceCoordinatorId != null) {
|
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
|
// Recipient 2: Docusign Recipient #1
|
||||||
Id docusignRecipientId = (Id) caseRecord.get(FIELD_DOCUSIGN_RECIPIENT);
|
Id docusignRecipientId = (Id) caseRecord.get(FIELD_DOCUSIGN_RECIPIENT);
|
||||||
if (docusignRecipientId != null) {
|
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()) {
|
if (recipients.isEmpty()) {
|
||||||
|
|
@ -297,9 +326,14 @@ global with sharing class DocusignCompositeEnvelopeBuilder {
|
||||||
* @param roleName The Docusign template role name
|
* @param roleName The Docusign template role name
|
||||||
* @param routingOrder Signing order
|
* @param routingOrder Signing order
|
||||||
* @param sourceRecordId The source Client_Case__c record ID
|
* @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
|
* @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
|
// Determine if this is a Contact or User
|
||||||
String objectType = recipientId.getSObjectType().getDescribe().getName();
|
String objectType = recipientId.getSObjectType().getDescribe().getName();
|
||||||
|
|
||||||
|
|
@ -320,8 +354,14 @@ global with sharing class DocusignCompositeEnvelopeBuilder {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (String.isBlank(recipientEmail)) {
|
if (String.isBlank(recipientEmail)) {
|
||||||
throw new IllegalArgumentException('No email found for ' + roleName + ' (' + recipientName + '). '
|
if (roleName == ROLE_DOCUSIGN_RECIPIENT && String.isNotBlank(smsPhone)) {
|
||||||
+ 'Please ensure the recipient has a valid email address.');
|
// 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(
|
return dfsle.Recipient.fromSource(
|
||||||
|
|
|
||||||
|
|
@ -39,4 +39,11 @@ global class DocusignEnvelopeRequest {
|
||||||
required=false
|
required=false
|
||||||
)
|
)
|
||||||
global Integer authReleaseFormCopies;
|
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>
|
<elementReference>authReleaseFormCopies</elementReference>
|
||||||
</value>
|
</value>
|
||||||
</inputParameters>
|
</inputParameters>
|
||||||
|
<inputParameters>
|
||||||
|
<name>recipientSmsPhone</name>
|
||||||
|
<value>
|
||||||
|
<elementReference>recipientSmsPhone</elementReference>
|
||||||
|
</value>
|
||||||
|
</inputParameters>
|
||||||
<nameSegment>DocusignCompositeEnvelopeBuilder</nameSegment>
|
<nameSegment>DocusignCompositeEnvelopeBuilder</nameSegment>
|
||||||
<offset>0</offset>
|
<offset>0</offset>
|
||||||
<outputParameters>
|
<outputParameters>
|
||||||
|
|
@ -179,7 +185,7 @@
|
||||||
<name>Is_Language_Selected</name>
|
<name>Is_Language_Selected</name>
|
||||||
<label>Is Language Selected?</label>
|
<label>Is Language Selected?</label>
|
||||||
<locationX>611</locationX>
|
<locationX>611</locationX>
|
||||||
<locationY>242</locationY>
|
<locationY>458</locationY>
|
||||||
<defaultConnector>
|
<defaultConnector>
|
||||||
<targetReference>Language_Not_Added_Screen</targetReference>
|
<targetReference>Language_Not_Added_Screen</targetReference>
|
||||||
</defaultConnector>
|
</defaultConnector>
|
||||||
|
|
@ -225,6 +231,109 @@
|
||||||
<label>Yes</label>
|
<label>Yes</label>
|
||||||
</rules>
|
</rules>
|
||||||
</decisions>
|
</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>
|
<environments>Default</environments>
|
||||||
<interviewLabel>Docusign Envelope Templates V3 {!$Flow.CurrentDateTime}</interviewLabel>
|
<interviewLabel>Docusign Envelope Templates V3 {!$Flow.CurrentDateTime}</interviewLabel>
|
||||||
<label>Docusign Envelope Templates V3</label>
|
<label>Docusign Envelope Templates V3</label>
|
||||||
|
|
@ -315,7 +424,7 @@
|
||||||
<locationY>134</locationY>
|
<locationY>134</locationY>
|
||||||
<assignNullValuesIfNoRecordsFound>false</assignNullValuesIfNoRecordsFound>
|
<assignNullValuesIfNoRecordsFound>false</assignNullValuesIfNoRecordsFound>
|
||||||
<connector>
|
<connector>
|
||||||
<targetReference>Is_Language_Selected</targetReference>
|
<targetReference>Get_Recipient_Contact</targetReference>
|
||||||
</connector>
|
</connector>
|
||||||
<filterLogic>and</filterLogic>
|
<filterLogic>and</filterLogic>
|
||||||
<filters>
|
<filters>
|
||||||
|
|
@ -329,6 +438,7 @@
|
||||||
<object>Client_Case__c</object>
|
<object>Client_Case__c</object>
|
||||||
<queriedFields>Id</queriedFields>
|
<queriedFields>Id</queriedFields>
|
||||||
<queriedFields>Docusign_Envelope_Language__c</queriedFields>
|
<queriedFields>Docusign_Envelope_Language__c</queriedFields>
|
||||||
|
<queriedFields>Docusign_Recipient_1__c</queriedFields>
|
||||||
<storeOutputAutomatically>true</storeOutputAutomatically>
|
<storeOutputAutomatically>true</storeOutputAutomatically>
|
||||||
</recordLookups>
|
</recordLookups>
|
||||||
<screens>
|
<screens>
|
||||||
|
|
@ -636,6 +746,13 @@
|
||||||
<booleanValue>false</booleanValue>
|
<booleanValue>false</booleanValue>
|
||||||
</value>
|
</value>
|
||||||
</variables>
|
</variables>
|
||||||
|
<variables>
|
||||||
|
<name>recipientSmsPhone</name>
|
||||||
|
<dataType>String</dataType>
|
||||||
|
<isCollection>false</isCollection>
|
||||||
|
<isInput>false</isInput>
|
||||||
|
<isOutput>false</isOutput>
|
||||||
|
</variables>
|
||||||
<choices>
|
<choices>
|
||||||
<name>AuthCopies_1</name>
|
<name>AuthCopies_1</name>
|
||||||
<choiceText>1 copy</choiceText>
|
<choiceText>1 copy</choiceText>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue