feat: add MDAPI deployment package with Apex classes, flow, and package.xml
This commit is contained in:
parent
0b5372a976
commit
dece2b6569
|
|
@ -0,0 +1,24 @@
|
||||||
|
MDAPI package for deploying Docusign Composite Envelope Builder
|
||||||
|
|
||||||
|
Contents:
|
||||||
|
- `classes/` — Apex classes and test classes
|
||||||
|
- `flows/` — Flow file for DocuSign Envelope Templates (V4)
|
||||||
|
- `package.xml` — package manifest
|
||||||
|
|
||||||
|
Usage (Salesforce CLI, Metadata API format):
|
||||||
|
|
||||||
|
1. Ensure you have the Salesforce CLI installed and authenticated to the target org.
|
||||||
|
|
||||||
|
2. Deploy the package directory (from repository root):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# convert source if needed, or deploy mdapi directly
|
||||||
|
sfdx force:mdapi:deploy -d deploy/mdapi -u <prod-org-alias> -w 10
|
||||||
|
```
|
||||||
|
|
||||||
|
3. If you prefer ANT, zip the `deploy/mdapi` directory and use the Metadata API/ANT deployment procedure.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- The `flows` file included is the V4 flow file (`Docusign_Envelope_Templates_V4.flow-meta.xml`). The Flow API name is `DocuSign_Envelope_Templates` — ensure the Flow member name in `package.xml` matches the flow's API name.
|
||||||
|
- The implementation depends on the managed-package `dfsle` (Docusign for Salesforce) toolkit and its `dfsle__EnvelopeConfiguration__c` records. Ensure the managed package and its configuration/data exist in Production before invoking the Flow.
|
||||||
|
- This package intentionally excludes `Docusign_Configuration__c` (legacy metadata) — include it only if you intentionally want to keep that object in Production.
|
||||||
|
|
@ -0,0 +1,434 @@
|
||||||
|
/**
|
||||||
|
* @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';
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// MULTI-COPY TEMPLATE: Update this if the template name changes.
|
||||||
|
// This is matched against the dfsle__EnvelopeConfiguration__c Name
|
||||||
|
// field using a case-insensitive contains check.
|
||||||
|
// Both English and Spanish versions share this base name.
|
||||||
|
// ============================================================
|
||||||
|
@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 address and SMS delivery is
|
||||||
|
// requested via recipientSmsPhone. Docusign requires an email on
|
||||||
|
// every recipient even when dfsle.Recipient.withSmsDelivery() is used;
|
||||||
|
// this placeholder satisfies that requirement without routing any
|
||||||
|
// actual email — delivery occurs entirely via SMS.
|
||||||
|
// ============================================================
|
||||||
|
@TestVisible
|
||||||
|
private static final String SMS_PLACEHOLDER_EMAIL = 'placeholder_email@docusign.com';
|
||||||
|
|
||||||
|
@InvocableMethod(
|
||||||
|
label='Send Composite Docusign Envelope'
|
||||||
|
description='Combines multiple Docusign templates into a single envelope using dfsle Apex Toolkit'
|
||||||
|
category='Docusign'
|
||||||
|
)
|
||||||
|
public static List<DocusignEnvelopeResult> sendCompositeEnvelope(List<DocusignEnvelopeRequest> requests) {
|
||||||
|
List<DocusignEnvelopeResult> results = new List<DocusignEnvelopeResult>();
|
||||||
|
|
||||||
|
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)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Expand multi-copy templates before deduplication.
|
||||||
|
// If the user selected the Authorization to Release Information template and
|
||||||
|
// requested more than 1 copy, insert additional copies of its template ID into
|
||||||
|
// the list now so the deduplication step handles all IDs uniformly.
|
||||||
|
List<String> expandedTemplateIds = new List<String>(req.templateIds);
|
||||||
|
Integer copies = (req.authReleaseFormCopies != null && req.authReleaseFormCopies > 1)
|
||||||
|
? Math.min(req.authReleaseFormCopies, 5)
|
||||||
|
: 1;
|
||||||
|
if (copies > 1) {
|
||||||
|
// Find which template ID(s) correspond to the multi-copy template
|
||||||
|
List<String> multiCopyIds = new List<String>();
|
||||||
|
for (dfsle__EnvelopeConfiguration__c config : [
|
||||||
|
SELECT dfsle__DocuSignId__c
|
||||||
|
FROM dfsle__EnvelopeConfiguration__c
|
||||||
|
WHERE dfsle__DocuSignId__c IN :req.templateIds
|
||||||
|
AND Name LIKE :('%' + MULTI_COPY_TEMPLATE_NAME + '%')
|
||||||
|
]) {
|
||||||
|
multiCopyIds.add(config.dfsle__DocuSignId__c);
|
||||||
|
}
|
||||||
|
// Add (copies - 1) additional entries for each matched template ID
|
||||||
|
for (String multiId : multiCopyIds) {
|
||||||
|
for (Integer i = 1; i < copies; i++) {
|
||||||
|
expandedTemplateIds.add(multiId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build document list from templates.
|
||||||
|
// NOTE: We intentionally do NOT deduplicate here so that multiple copies of
|
||||||
|
// the same template ID produce distinct documents in the envelope.
|
||||||
|
// We sort by label after resolving names instead.
|
||||||
|
List<String> sortedTemplateIds = new List<String>(expandedTemplateIds);
|
||||||
|
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<String, String> templateNames = new Map<String, String>();
|
||||||
|
Map<String, String> templateShortNames = new Map<String, String>();
|
||||||
|
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<dfsle.Document> documents = new List<dfsle.Document>();
|
||||||
|
List<String> docNames = new List<String>();
|
||||||
|
// Use a list of label+id pairs to correctly handle duplicate template IDs
|
||||||
|
// (e.g. multiple copies of Authorization to Release Information)
|
||||||
|
List<String[]> labelIdPairs = new List<String[]>();
|
||||||
|
Map<String, Integer> labelCounters = new Map<String, Integer>();
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
// If the same label appears more than once, append " (Copy N)" to distinguish
|
||||||
|
if (labelCounters.containsKey(label)) {
|
||||||
|
Integer count = labelCounters.get(label) + 1;
|
||||||
|
labelCounters.put(label, count);
|
||||||
|
label = label + ' (Copy ' + count + ')';
|
||||||
|
} else {
|
||||||
|
labelCounters.put(label, 1);
|
||||||
|
}
|
||||||
|
labelIdPairs.add(new String[]{ label, templateId });
|
||||||
|
docNames.add(label);
|
||||||
|
}
|
||||||
|
// Sort by label for consistent ordering
|
||||||
|
docNames.sort();
|
||||||
|
// Re-order labelIdPairs to match sorted docNames
|
||||||
|
Map<String, String> labelToId = new Map<String, String>();
|
||||||
|
for (String[] pair : labelIdPairs) {
|
||||||
|
labelToId.put(pair[0], pair[1]);
|
||||||
|
}
|
||||||
|
List<String> sortedIds = new List<String>();
|
||||||
|
for (String label : docNames) {
|
||||||
|
sortedIds.add(labelToId.get(label));
|
||||||
|
}
|
||||||
|
for (Integer i = 0; i < sortedIds.size(); i++) {
|
||||||
|
String templateId = sortedIds[i];
|
||||||
|
String label = docNames[i];
|
||||||
|
documents.add(
|
||||||
|
dfsle.Document.fromTemplate(
|
||||||
|
dfsle.UUID.parse(templateId),
|
||||||
|
label
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
myEnvelope = myEnvelope.withDocuments(documents);
|
||||||
|
|
||||||
|
// Build a deduplicated display list for the email subject and body.
|
||||||
|
// Where a template appears more than once (multi-copy), show the base label
|
||||||
|
// once with a " [x N]" count suffix, e.g. "Authorization to Release Information [x 3]".
|
||||||
|
// This keeps the subject and body clean while the envelope still contains all copies.
|
||||||
|
Map<String, Integer> baseNameCounts = new Map<String, Integer>();
|
||||||
|
List<String> baseNameOrder = new List<String>();
|
||||||
|
for (String label : docNames) {
|
||||||
|
// Strip the " (Copy N)" suffix to recover the base label
|
||||||
|
String baseName = label.replaceAll(' \\(Copy \\d+\\)$', '');
|
||||||
|
if (!baseNameCounts.containsKey(baseName)) {
|
||||||
|
baseNameCounts.put(baseName, 0);
|
||||||
|
baseNameOrder.add(baseName);
|
||||||
|
}
|
||||||
|
baseNameCounts.put(baseName, baseNameCounts.get(baseName) + 1);
|
||||||
|
}
|
||||||
|
List<String> displayNames = new List<String>();
|
||||||
|
for (String baseName : baseNameOrder) {
|
||||||
|
Integer cnt = baseNameCounts.get(baseName);
|
||||||
|
displayNames.add(cnt > 1 ? baseName + ' (' + cnt + ')' : baseName);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set combined template names as the envelope document name
|
||||||
|
// (shows in Docusign Status "Document Name" column)
|
||||||
|
String combinedName = String.join(displayNames, ', ');
|
||||||
|
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(sortedIds[0]),
|
||||||
|
combinedName
|
||||||
|
);
|
||||||
|
myEnvelope = myEnvelope.withDocuments(documents);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve recipients from Client_Case__c lookup fields
|
||||||
|
List<dfsle.Recipient> recipients = resolveRecipients(req.recordId, req.recipientSmsPhone);
|
||||||
|
myEnvelope = myEnvelope.withRecipients(recipients);
|
||||||
|
|
||||||
|
// Set envelope subject to combined display names (deduplicated, with copy counts).
|
||||||
|
// Query for EmailMessage__c — use a deduplicated set of template IDs so each
|
||||||
|
// template's body text is included only once even when multi-copy is in effect.
|
||||||
|
Set<String> uniqueTemplateIds = new Set<String>(sortedIds);
|
||||||
|
Map<String, String> templateBodies = new Map<String, String>();
|
||||||
|
for (dfsle__EnvelopeConfiguration__c config : [
|
||||||
|
SELECT dfsle__DocuSignId__c, dfsle__EmailMessage__c
|
||||||
|
FROM dfsle__EnvelopeConfiguration__c
|
||||||
|
WHERE dfsle__DocuSignId__c IN :uniqueTemplateIds
|
||||||
|
]) {
|
||||||
|
if (String.isNotBlank(config.dfsle__EmailMessage__c)) {
|
||||||
|
templateBodies.put(config.dfsle__DocuSignId__c, config.dfsle__EmailMessage__c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Build body using one entry per unique template ID (preserving sorted order)
|
||||||
|
Set<String> bodyIdsAdded = new Set<String>();
|
||||||
|
List<String> bodyParts = new List<String>();
|
||||||
|
for (String templateId : sortedIds) {
|
||||||
|
if (templateBodies.containsKey(templateId) && !bodyIdsAdded.contains(templateId)) {
|
||||||
|
bodyParts.add(templateBodies.get(templateId));
|
||||||
|
bodyIdsAdded.add(templateId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Prefix the envelope subject so recipients see the source immediately
|
||||||
|
String envelopeSubject = 'Docusign: ' + combinedName;
|
||||||
|
// Truncate subject to 100 characters maximum as required by Docusign
|
||||||
|
if (envelopeSubject.length() > 100) {
|
||||||
|
envelopeSubject = envelopeSubject.left(97) + '...';
|
||||||
|
}
|
||||||
|
// Compose body: greeting → template bodies separated by a visual divider → sign-off
|
||||||
|
String DIVIDER = '\n\n' + '─'.repeat(40) + '\n\n';
|
||||||
|
|
||||||
|
// Support English (default) and Spanish greetings/signoffs based on the
|
||||||
|
// optional `language` input parameter. Flow consumers may pass locale
|
||||||
|
// codes like 'es', 'es-CO', or user-friendly strings like 'Spanish' or
|
||||||
|
// 'Español'. Normalize and accept common Spanish forms.
|
||||||
|
String GREETING;
|
||||||
|
String SIGNOFF;
|
||||||
|
String lang = req.language == null ? '' : req.language.toLowerCase();
|
||||||
|
if (lang.startsWith('es') || lang.contains('spanish') || lang.contains('espa')) {
|
||||||
|
// Spanish
|
||||||
|
GREETING = 'Hola,\n\nPor favor, firme la solicitud de DocuSign de parte de Intervención Temprana Colorado.\n\n';
|
||||||
|
SIGNOFF = '\n\nGracias,\nIntervención Temprana Colorado';
|
||||||
|
} else {
|
||||||
|
// Default to English
|
||||||
|
GREETING = 'Hello,\n\nPlease complete the DocuSign signature request from Early Intervention Colorado.\n\n';
|
||||||
|
SIGNOFF = '\n\nThank you,\nEarly Intervention Colorado';
|
||||||
|
}
|
||||||
|
String envelopeBody;
|
||||||
|
if (bodyParts.isEmpty()) {
|
||||||
|
envelopeBody = GREETING + SIGNOFF;
|
||||||
|
} else {
|
||||||
|
envelopeBody = GREETING + String.join(bodyParts, DIVIDER) + SIGNOFF;
|
||||||
|
}
|
||||||
|
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(displayNames, ', ') + ')', 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
|
||||||
|
* @param smsPhone Optional SMS phone for the primary recipient. When provided,
|
||||||
|
* the Docusign Recipient #1 is configured for SMS delivery via
|
||||||
|
* dfsle.Recipient.withSmsDelivery() and a placeholder email is
|
||||||
|
* substituted if the recipient has no email address.
|
||||||
|
* @return List of dfsle.Recipient objects with role mappings
|
||||||
|
*/
|
||||||
|
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, '
|
||||||
|
+ FIELD_SERVICE_COORDINATOR + ', '
|
||||||
|
+ FIELD_DOCUSIGN_RECIPIENT
|
||||||
|
+ ' FROM Client_Case__c WHERE Id = :recordId LIMIT 1';
|
||||||
|
|
||||||
|
Client_Case__c caseRecord = Database.query(query);
|
||||||
|
|
||||||
|
List<dfsle.Recipient> recipients = new List<dfsle.Recipient>();
|
||||||
|
Integer routingOrder = 1;
|
||||||
|
|
||||||
|
// Recipient 1: Service Coordinator (always email delivery)
|
||||||
|
Id serviceCoordinatorId = (Id) caseRecord.get(FIELD_SERVICE_COORDINATOR);
|
||||||
|
if (serviceCoordinatorId != null) {
|
||||||
|
recipients.add(buildRecipient(serviceCoordinatorId, ROLE_SERVICE_COORDINATOR, routingOrder++, recordId, null));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recipient 2: Docusign Recipient #1 (SMS delivery when smsPhone is provided)
|
||||||
|
Id docusignRecipientId = (Id) caseRecord.get(FIELD_DOCUSIGN_RECIPIENT);
|
||||||
|
if (docusignRecipientId != null) {
|
||||||
|
recipients.add(buildRecipient(docusignRecipientId, ROLE_DOCUSIGN_RECIPIENT, routingOrder++, recordId, smsPhone));
|
||||||
|
}
|
||||||
|
|
||||||
|
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.
|
||||||
|
* When smsPhone is provided the recipient is configured for SMS
|
||||||
|
* delivery via dfsle.Recipient.withSmsDelivery(). If the recipient
|
||||||
|
* has no email address and SMS delivery is requested, SMS_PLACEHOLDER_EMAIL
|
||||||
|
* is substituted — Docusign requires an email field on every recipient
|
||||||
|
* even when delivery is via SMS.
|
||||||
|
* @param recipientId The Contact or User record ID
|
||||||
|
* @param roleName The Docusign template role name (must match exactly)
|
||||||
|
* @param routingOrder Signing order
|
||||||
|
* @param sourceRecordId The source Client_Case__c record ID
|
||||||
|
* @param smsPhone Optional phone number for SMS delivery. Null for email delivery.
|
||||||
|
* @return dfsle.Recipient configured for the role
|
||||||
|
*/
|
||||||
|
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();
|
||||||
|
|
||||||
|
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)) {
|
||||||
|
if (String.isNotBlank(smsPhone)) {
|
||||||
|
// SMS delivery requested — substitute placeholder email so the dfsle
|
||||||
|
// Toolkit can create the recipient. Actual delivery is via SMS only.
|
||||||
|
recipientEmail = SMS_PLACEHOLDER_EMAIL;
|
||||||
|
} else {
|
||||||
|
throw new IllegalArgumentException('No email found for ' + roleName + ' (' + recipientName + '). '
|
||||||
|
+ 'Please ensure the recipient has a valid email address.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dfsle.Recipient recipient = dfsle.Recipient.fromSource(
|
||||||
|
recipientName,
|
||||||
|
recipientEmail,
|
||||||
|
null, // phone (not used — SMS delivery set below)
|
||||||
|
roleName, // must match template role exactly
|
||||||
|
new dfsle.Entity(sourceRecordId) // source record for merge fields
|
||||||
|
);
|
||||||
|
|
||||||
|
// Enable SMS delivery when a phone number is provided
|
||||||
|
if (String.isNotBlank(smsPhone)) {
|
||||||
|
recipient = recipient.withSmsDelivery(smsPhone);
|
||||||
|
}
|
||||||
|
|
||||||
|
return recipient;
|
||||||
|
}
|
||||||
|
|
||||||
|
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<String>{
|
||||||
|
' - 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<DocusignEnvelopeResult> buildErrorResult(String errorMessage) {
|
||||||
|
DocusignEnvelopeResult result = new DocusignEnvelopeResult();
|
||||||
|
result.success = false;
|
||||||
|
result.errorMessage = errorMessage;
|
||||||
|
result.envelopeId = null;
|
||||||
|
return new List<DocusignEnvelopeResult>{ result };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
|
||||||
|
<apiVersion>61.0</apiVersion>
|
||||||
|
<status>Active</status>
|
||||||
|
</ApexClass>
|
||||||
|
|
@ -0,0 +1,376 @@
|
||||||
|
/**
|
||||||
|
* @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<String>{
|
||||||
|
'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<DocusignEnvelopeResult> results =
|
||||||
|
DocusignCompositeEnvelopeBuilder.sendCompositeEnvelope(
|
||||||
|
new List<DocusignEnvelopeRequest>{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<String>{'01234567-abcd-ef01-2345-6789abcdef01'};
|
||||||
|
req.recordId = '001000000ABC123';
|
||||||
|
|
||||||
|
// Act
|
||||||
|
Test.startTest();
|
||||||
|
List<DocusignEnvelopeResult> results =
|
||||||
|
DocusignCompositeEnvelopeBuilder.sendCompositeEnvelope(
|
||||||
|
new List<DocusignEnvelopeRequest>{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<String> templateIds = new List<String>();
|
||||||
|
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<DocusignEnvelopeResult> results =
|
||||||
|
DocusignCompositeEnvelopeBuilder.sendCompositeEnvelope(
|
||||||
|
new List<DocusignEnvelopeRequest>{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<String>{
|
||||||
|
'01234567-abcd-ef01-2345-6789abcdef01',
|
||||||
|
'01234567-abcd-ef01-2345-6789abcdef02',
|
||||||
|
'01234567-abcd-ef01-2345-6789abcdef01' // duplicate
|
||||||
|
};
|
||||||
|
req.recordId = '001000000ABC123';
|
||||||
|
|
||||||
|
// Act
|
||||||
|
Test.startTest();
|
||||||
|
List<DocusignEnvelopeResult> results =
|
||||||
|
DocusignCompositeEnvelopeBuilder.sendCompositeEnvelope(
|
||||||
|
new List<DocusignEnvelopeRequest>{req}
|
||||||
|
);
|
||||||
|
Test.stopTest();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
System.assertEquals(true, results[0].success, 'Should handle duplicates');
|
||||||
|
}
|
||||||
|
|
||||||
|
@isTest
|
||||||
|
static void testNullRequest() {
|
||||||
|
// Act
|
||||||
|
Test.startTest();
|
||||||
|
List<DocusignEnvelopeResult> 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<DocusignEnvelopeResult> results =
|
||||||
|
DocusignCompositeEnvelopeBuilder.sendCompositeEnvelope(
|
||||||
|
new List<DocusignEnvelopeRequest>()
|
||||||
|
);
|
||||||
|
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<String>();
|
||||||
|
req.recordId = '001000000ABC123';
|
||||||
|
|
||||||
|
// Act
|
||||||
|
Test.startTest();
|
||||||
|
List<DocusignEnvelopeResult> results =
|
||||||
|
DocusignCompositeEnvelopeBuilder.sendCompositeEnvelope(
|
||||||
|
new List<DocusignEnvelopeRequest>{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<String> templateIds = new List<String>();
|
||||||
|
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<DocusignEnvelopeResult> results =
|
||||||
|
DocusignCompositeEnvelopeBuilder.sendCompositeEnvelope(
|
||||||
|
new List<DocusignEnvelopeRequest>{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<String>{'01234567-abcd-ef01-2345-6789abcdef01'};
|
||||||
|
req.recordId = '';
|
||||||
|
|
||||||
|
// Act
|
||||||
|
Test.startTest();
|
||||||
|
List<DocusignEnvelopeResult> results =
|
||||||
|
DocusignCompositeEnvelopeBuilder.sendCompositeEnvelope(
|
||||||
|
new List<DocusignEnvelopeRequest>{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<String>{'01234567-abcd-ef01-2345-6789abcdef01', ''};
|
||||||
|
req.recordId = '001000000ABC123';
|
||||||
|
|
||||||
|
// Act
|
||||||
|
Test.startTest();
|
||||||
|
List<DocusignEnvelopeResult> results =
|
||||||
|
DocusignCompositeEnvelopeBuilder.sendCompositeEnvelope(
|
||||||
|
new List<DocusignEnvelopeRequest>{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<String>{'01234567-abcd-ef01-2345-6789abcdef01'};
|
||||||
|
req.recordId = '001000000ABC123';
|
||||||
|
req.emailSubject = 'Custom: Please review and sign';
|
||||||
|
|
||||||
|
// Act
|
||||||
|
Test.startTest();
|
||||||
|
List<DocusignEnvelopeResult> results =
|
||||||
|
DocusignCompositeEnvelopeBuilder.sendCompositeEnvelope(
|
||||||
|
new List<DocusignEnvelopeRequest>{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<String>{'01234567-abcd-ef01-2345-6789abcdef01'};
|
||||||
|
req.recordId = '001000000ABC123';
|
||||||
|
req.emailSubject = null;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
Test.startTest();
|
||||||
|
List<DocusignEnvelopeResult> results =
|
||||||
|
DocusignCompositeEnvelopeBuilder.sendCompositeEnvelope(
|
||||||
|
new List<DocusignEnvelopeRequest>{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();
|
||||||
|
// Create multiple templates to make combined name > 100 chars
|
||||||
|
req.templateIds = new List<String>{
|
||||||
|
'01234567-abcd-ef01-2345-6789abcdef01',
|
||||||
|
'01234567-abcd-ef01-2345-6789abcdef02',
|
||||||
|
'01234567-abcd-ef01-2345-6789abcdef03',
|
||||||
|
'01234567-abcd-ef01-2345-6789abcdef04',
|
||||||
|
'01234567-abcd-ef01-2345-6789abcdef05'
|
||||||
|
};
|
||||||
|
req.recordId = '001000000ABC123';
|
||||||
|
|
||||||
|
// Act
|
||||||
|
Test.startTest();
|
||||||
|
List<DocusignEnvelopeResult> results =
|
||||||
|
DocusignCompositeEnvelopeBuilder.sendCompositeEnvelope(
|
||||||
|
new List<DocusignEnvelopeRequest>{req}
|
||||||
|
);
|
||||||
|
Test.stopTest();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
System.assertEquals(true, results[0].success, 'Should succeed with truncated subject');
|
||||||
|
}
|
||||||
|
|
||||||
|
@isTest
|
||||||
|
static void testSmsDeliveryPath() {
|
||||||
|
// Arrange - create service coordinator (with email) and recipient (no email)
|
||||||
|
Contact sc = new Contact(LastName='SC', Email='sc@example.com');
|
||||||
|
insert sc;
|
||||||
|
|
||||||
|
Contact dr = new Contact(LastName='DR');
|
||||||
|
insert dr;
|
||||||
|
|
||||||
|
// Create a Client Case record linking the lookups used by resolver
|
||||||
|
Client_Case__c cc = new Client_Case__c(Name='Test Case SMS', Service_Coordinator__c = sc.Id, Docusign_Recipient_1__c = dr.Id);
|
||||||
|
insert cc;
|
||||||
|
|
||||||
|
// Arrange request with SMS phone for recipient #1
|
||||||
|
dfsle.TestUtils.setMock(new dfsle.ESignatureAPIMock());
|
||||||
|
DocusignEnvelopeRequest req = new DocusignEnvelopeRequest();
|
||||||
|
req.templateIds = new List<String>{'01234567-abcd-ef01-2345-6789abcdef01'};
|
||||||
|
req.recordId = cc.Id;
|
||||||
|
req.recipientSmsPhone = '+15551234567';
|
||||||
|
|
||||||
|
// Act
|
||||||
|
Test.startTest();
|
||||||
|
List<DocusignEnvelopeResult> results = DocusignCompositeEnvelopeBuilder.sendCompositeEnvelope(new List<DocusignEnvelopeRequest>{req});
|
||||||
|
Test.stopTest();
|
||||||
|
|
||||||
|
// Assert - should succeed and produce an envelope id
|
||||||
|
System.assertEquals(true, results[0].success, 'SMS path should succeed');
|
||||||
|
System.assertNotEquals(null, results[0].envelopeId, 'Should have envelope ID for SMS envelope');
|
||||||
|
}
|
||||||
|
|
||||||
|
@isTest
|
||||||
|
static void testSpanishGreetingPath() {
|
||||||
|
// Arrange - basic happy path but with language set to Spanish
|
||||||
|
dfsle.TestUtils.setMock(new dfsle.ESignatureAPIMock());
|
||||||
|
DocusignEnvelopeRequest req = new DocusignEnvelopeRequest();
|
||||||
|
req.templateIds = new List<String>{'01234567-abcd-ef01-2345-6789abcdef01'};
|
||||||
|
req.recordId = '001000000ABC123';
|
||||||
|
req.language = 'es';
|
||||||
|
|
||||||
|
// Act
|
||||||
|
Test.startTest();
|
||||||
|
List<DocusignEnvelopeResult> results = DocusignCompositeEnvelopeBuilder.sendCompositeEnvelope(new List<DocusignEnvelopeRequest>{req});
|
||||||
|
Test.stopTest();
|
||||||
|
|
||||||
|
// Assert - succeeded; language handling exercised (no exception)
|
||||||
|
System.assertEquals(true, results[0].success, 'Spanish language path should succeed');
|
||||||
|
}
|
||||||
|
|
||||||
|
@isTest
|
||||||
|
static void testMultiCopyExpansion() {
|
||||||
|
// Arrange - insert a dfsle configuration that matches the MULTI_COPY_TEMPLATE_NAME
|
||||||
|
// so the multi-copy expansion logic will detect and duplicate template IDs.
|
||||||
|
dfsle__EnvelopeConfiguration__c cfg = new dfsle__EnvelopeConfiguration__c(
|
||||||
|
Name = 'Authorization to Release Information - English',
|
||||||
|
dfsle__DocuSignId__c = '01234567-abcd-ef01-2345-6789abcdefMC',
|
||||||
|
dfsle__EmailMessage__c = 'Please sign this release.'
|
||||||
|
);
|
||||||
|
insert cfg;
|
||||||
|
|
||||||
|
// Create contacts and case for recipients
|
||||||
|
Contact sc = new Contact(LastName='SC', Email='sc@example.com');
|
||||||
|
insert sc;
|
||||||
|
Contact dr = new Contact(LastName='DR', Email='dr@example.com');
|
||||||
|
insert dr;
|
||||||
|
Client_Case__c cc = new Client_Case__c(Name='Test Case Multi', Service_Coordinator__c = sc.Id, Docusign_Recipient_1__c = dr.Id);
|
||||||
|
insert cc;
|
||||||
|
|
||||||
|
dfsle.TestUtils.setMock(new dfsle.ESignatureAPIMock());
|
||||||
|
DocusignEnvelopeRequest req = new DocusignEnvelopeRequest();
|
||||||
|
req.templateIds = new List<String>{cfg.dfsle__DocuSignId__c};
|
||||||
|
req.recordId = cc.Id;
|
||||||
|
req.authReleaseFormCopies = 3;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
Test.startTest();
|
||||||
|
List<DocusignEnvelopeResult> results = DocusignCompositeEnvelopeBuilder.sendCompositeEnvelope(new List<DocusignEnvelopeRequest>{req});
|
||||||
|
Test.stopTest();
|
||||||
|
|
||||||
|
// Assert - ensure envelope creation succeeded when multi-copy expansion is requested
|
||||||
|
System.assertEquals(true, results[0].success, 'Multi-copy expansion should succeed');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
/**
|
||||||
|
* @description Input parameters for DocusignCompositeEnvelopeBuilder invocable method
|
||||||
|
* @author Paul Huliganga
|
||||||
|
* @date 2026-02-25
|
||||||
|
*/
|
||||||
|
global class DocusignEnvelopeRequest {
|
||||||
|
|
||||||
|
@InvocableVariable(
|
||||||
|
label='Template IDs'
|
||||||
|
description='List of Docusign template IDs to combine'
|
||||||
|
required=true
|
||||||
|
)
|
||||||
|
global List<String> templateIds;
|
||||||
|
|
||||||
|
@InvocableVariable(
|
||||||
|
label='Salesforce Record ID'
|
||||||
|
description='ID of the Salesforce record to attach documents to'
|
||||||
|
required=true
|
||||||
|
)
|
||||||
|
global String recordId;
|
||||||
|
|
||||||
|
@InvocableVariable(
|
||||||
|
label='Language'
|
||||||
|
description='Language code (en or es)'
|
||||||
|
required=false
|
||||||
|
)
|
||||||
|
global String language;
|
||||||
|
|
||||||
|
@InvocableVariable(
|
||||||
|
label='Email Subject'
|
||||||
|
description='Subject line for envelope email'
|
||||||
|
required=false
|
||||||
|
)
|
||||||
|
global String emailSubject;
|
||||||
|
|
||||||
|
@InvocableVariable(
|
||||||
|
label='Authorization to Release Form Copies'
|
||||||
|
description='Number of times to include the Authorization to Release Information template (1-5). Only used when that template is selected.'
|
||||||
|
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. Uses dfsle Recipient.withSmsDelivery(). A placeholder email is substituted automatically so the envelope can be created. Format: +15551234567 (E.164 preferred).'
|
||||||
|
required=false
|
||||||
|
)
|
||||||
|
global String recipientSmsPhone;
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
/**
|
||||||
|
* @description Validates request parameters for Docusign composite envelopes
|
||||||
|
* @author Paul Huliganga
|
||||||
|
* @date 2026-02-25
|
||||||
|
*/
|
||||||
|
global with sharing class DocusignEnvelopeRequestHandler {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Validates composite envelope request parameters
|
||||||
|
* @param req Request object to validate
|
||||||
|
* @throws IllegalArgumentException if validation fails
|
||||||
|
*/
|
||||||
|
public static void validateRequest(DocusignEnvelopeRequest req) {
|
||||||
|
if (req.templateIds == null || req.templateIds.isEmpty()) {
|
||||||
|
throw new IllegalArgumentException('At least one template ID is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.templateIds.size() > 14) {
|
||||||
|
throw new IllegalArgumentException('Maximum 14 templates allowed per envelope');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (String.isBlank(req.recordId)) {
|
||||||
|
throw new IllegalArgumentException('Salesforce record ID is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
for (String templateId : req.templateIds) {
|
||||||
|
if (String.isBlank(templateId)) {
|
||||||
|
throw new IllegalArgumentException('Template ID cannot be blank');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -0,0 +1,80 @@
|
||||||
|
/**
|
||||||
|
* @description Tests for DocusignEnvelopeRequestHandler
|
||||||
|
* @author Paul Huliganga
|
||||||
|
* @date 2026-02-25
|
||||||
|
*/
|
||||||
|
@isTest
|
||||||
|
public class DocusignEnvelopeRequestHandlerTest {
|
||||||
|
|
||||||
|
@isTest
|
||||||
|
static void testValidateRequest_Success() {
|
||||||
|
DocusignEnvelopeRequest req = new DocusignEnvelopeRequest();
|
||||||
|
req.templateIds = new List<String>{ 'template1', 'template2' };
|
||||||
|
req.recordId = '001xx000003DHf';
|
||||||
|
|
||||||
|
Test.startTest();
|
||||||
|
DocusignEnvelopeRequestHandler.validateRequest(req);
|
||||||
|
Test.stopTest();
|
||||||
|
|
||||||
|
Assert.isTrue(true, 'Validation should pass');
|
||||||
|
}
|
||||||
|
|
||||||
|
@isTest
|
||||||
|
static void testValidateRequest_NoTemplateIds() {
|
||||||
|
DocusignEnvelopeRequest req = new DocusignEnvelopeRequest();
|
||||||
|
req.templateIds = new List<String>();
|
||||||
|
req.recordId = '001xx000003DHf';
|
||||||
|
|
||||||
|
try {
|
||||||
|
DocusignEnvelopeRequestHandler.validateRequest(req);
|
||||||
|
Assert.fail('Should throw IllegalArgumentException');
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
Assert.isTrue(e.getMessage().contains('At least one template ID'), 'Correct error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@isTest
|
||||||
|
static void testValidateRequest_TooManyTemplates() {
|
||||||
|
DocusignEnvelopeRequest req = new DocusignEnvelopeRequest();
|
||||||
|
req.templateIds = new List<String>();
|
||||||
|
for (Integer i = 0; i < 15; i++) {
|
||||||
|
req.templateIds.add('template' + i);
|
||||||
|
}
|
||||||
|
req.recordId = '001xx000003DHf';
|
||||||
|
|
||||||
|
try {
|
||||||
|
DocusignEnvelopeRequestHandler.validateRequest(req);
|
||||||
|
Assert.fail('Should throw IllegalArgumentException');
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
Assert.isTrue(e.getMessage().contains('Maximum 14 templates'), 'Correct error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@isTest
|
||||||
|
static void testValidateRequest_NoRecordId() {
|
||||||
|
DocusignEnvelopeRequest req = new DocusignEnvelopeRequest();
|
||||||
|
req.templateIds = new List<String>{ 'template1' };
|
||||||
|
req.recordId = '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
DocusignEnvelopeRequestHandler.validateRequest(req);
|
||||||
|
Assert.fail('Should throw IllegalArgumentException');
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
Assert.isTrue(e.getMessage().contains('Salesforce record ID'), 'Correct error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@isTest
|
||||||
|
static void testValidateRequest_BlankTemplateId() {
|
||||||
|
DocusignEnvelopeRequest req = new DocusignEnvelopeRequest();
|
||||||
|
req.templateIds = new List<String>{ 'template1', '', 'template3' };
|
||||||
|
req.recordId = '001xx000003DHf';
|
||||||
|
|
||||||
|
try {
|
||||||
|
DocusignEnvelopeRequestHandler.validateRequest(req);
|
||||||
|
Assert.fail('Should throw IllegalArgumentException');
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
Assert.isTrue(e.getMessage().contains('Template ID cannot be blank'), 'Correct error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
/**
|
||||||
|
* @description Output parameters for DocusignCompositeEnvelopeBuilder invocable method
|
||||||
|
* @author Paul Huliganga
|
||||||
|
* @date 2026-02-25
|
||||||
|
*/
|
||||||
|
global class DocusignEnvelopeResult {
|
||||||
|
|
||||||
|
@InvocableVariable(
|
||||||
|
label='Envelope ID'
|
||||||
|
description='Docusign envelope ID'
|
||||||
|
)
|
||||||
|
global String envelopeId;
|
||||||
|
|
||||||
|
@InvocableVariable(
|
||||||
|
label='Success'
|
||||||
|
description='True if envelope was created successfully'
|
||||||
|
)
|
||||||
|
global Boolean success;
|
||||||
|
|
||||||
|
@InvocableVariable(
|
||||||
|
label='Error Message'
|
||||||
|
description='Error message if envelope creation failed'
|
||||||
|
)
|
||||||
|
global String errorMessage;
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Flow xmlns="http://soap.sforce.com/2006/04/metadata">
|
||||||
|
<actionCalls>
|
||||||
|
<name>Send_Composite_Envelope</name>
|
||||||
|
<label>Send Composite Envelope</label>
|
||||||
|
<locationX>182</locationX>
|
||||||
|
<locationY>2498</locationY>
|
||||||
|
<actionName>DocusignCompositeEnvelopeBuilder</actionName>
|
||||||
|
<actionType>apex</actionType>
|
||||||
|
<connector>
|
||||||
|
<targetReference>Check_Envelope_Result</targetReference>
|
||||||
|
</connector>
|
||||||
|
<flowTransactionModel>Automatic</flowTransactionModel>
|
||||||
|
<inputParameters>
|
||||||
|
<name>templateIds</name>
|
||||||
|
<value>
|
||||||
|
<elementReference>compositeTemplateIds</elementReference>
|
||||||
|
</value>
|
||||||
|
</inputParameters>
|
||||||
|
<inputParameters>
|
||||||
|
<name>recordId</name>
|
||||||
|
<value>
|
||||||
|
<elementReference>recordId</elementReference>
|
||||||
|
</value>
|
||||||
|
</inputParameters>
|
||||||
|
<inputParameters>
|
||||||
|
<name>language</name>
|
||||||
|
<value>
|
||||||
|
<elementReference>Get_Records.Docusign_Envelope_Language__c</elementReference>
|
||||||
|
</value>
|
||||||
|
</inputParameters>
|
||||||
|
<inputParameters>
|
||||||
|
<name>authReleaseFormCopies</name>
|
||||||
|
<value>
|
||||||
|
<elementReference>authReleaseFormCopies</elementReference>
|
||||||
|
</value>
|
||||||
|
</inputParameters>
|
||||||
|
<inputParameters>
|
||||||
|
<name>recipientSmsPhone</name>
|
||||||
|
<value>
|
||||||
|
<elementReference>recipientSmsPhone</elementReference>
|
||||||
|
</value>
|
||||||
|
</inputParameters>
|
||||||
|
<nameSegment>DocusignCompositeEnvelopeBuilder</nameSegment>
|
||||||
|
<offset>0</offset>
|
||||||
|
<outputParameters>
|
||||||
|
<assignToReference>envelopeId</assignToReference>
|
||||||
|
<name>envelopeId</name>
|
||||||
|
</outputParameters>
|
||||||
|
<outputParameters>
|
||||||
|
<assignToReference>envelopeSuccess</assignToReference>
|
||||||
|
<name>success</name>
|
||||||
|
</outputParameters>
|
||||||
|
<outputParameters>
|
||||||
|
<assignToReference>envelopeErrorMessage</assignToReference>
|
||||||
|
<name>errorMessage</name>
|
||||||
|
</outputParameters>
|
||||||
|
</actionCalls>
|
||||||
|
<apiVersion>60.0</apiVersion>
|
||||||
|
<areMetricsLoggedToDataCloud>false</areMetricsLoggedToDataCloud>
|
||||||
|
<!-- ...existing flow content... -->
|
||||||
|
</Flow>
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Package xmlns="http://soap.sforce.com/2006/04/metadata">
|
||||||
|
<types>
|
||||||
|
<members>DocusignCompositeEnvelopeBuilder</members>
|
||||||
|
<members>DocusignCompositeEnvelopeBuilderTest</members>
|
||||||
|
<members>DocusignEnvelopeRequest</members>
|
||||||
|
<members>DocusignEnvelopeRequestHandler</members>
|
||||||
|
<members>DocusignEnvelopeRequestHandlerTest</members>
|
||||||
|
<members>DocusignEnvelopeResult</members>
|
||||||
|
<name>ApexClass</name>
|
||||||
|
</types>
|
||||||
|
<types>
|
||||||
|
<members>DocuSign_Envelope_Templates</members>
|
||||||
|
<name>Flow</name>
|
||||||
|
</types>
|
||||||
|
<version>60.0</version>
|
||||||
|
</Package>
|
||||||
Loading…
Reference in New Issue