Compare commits
5 Commits
2eac94f719
...
d77f14b205
| Author | SHA1 | Date |
|---|---|---|
|
|
d77f14b205 | |
|
|
d70cd8a97a | |
|
|
1286925765 | |
|
|
dece2b6569 | |
|
|
0b5372a976 |
|
|
@ -4,23 +4,22 @@
|
|||
|
||||
All Apex classes and tests have been created! Here's what you have:
|
||||
|
||||
### 📦 Code Files Created (6 classes)
|
||||
### 📦 Code Files Included in This Package
|
||||
|
||||
**Main Classes:**
|
||||
1. `DocusignCompositeEnvelopeBuilder.cls` (11.5 KB) - Invocable method for Flow integration
|
||||
2. `DocusignAPIService.cls` (10.5 KB) - REST API service layer
|
||||
3. `DocusignCredentials.cls` (5.9 KB) - Credential management
|
||||
1. `DocusignCompositeEnvelopeBuilder.cls` - Invocable method for Flow integration (uses dfsle toolkit)
|
||||
2. `DocusignEnvelopeRequest.cls` - Invocable request parameter object
|
||||
3. `DocusignEnvelopeRequestHandler.cls` - Request validation helper
|
||||
4. `DocusignEnvelopeResult.cls` - Invocable result object
|
||||
|
||||
**Test Classes:**
|
||||
4. `DocusignCompositeEnvelopeBuilderTest.cls` (13.8 KB) - 12 test methods
|
||||
5. `DocusignAPIServiceTest.cls` (11.7 KB) - 14 test methods
|
||||
6. `DocusignCredentialsTest.cls` (8.0 KB) - 13 test methods
|
||||
- `DocusignCompositeEnvelopeBuilderTest.cls`
|
||||
- `DocusignEnvelopeRequestHandlerTest.cls`
|
||||
|
||||
**Total:** 39 test methods for comprehensive coverage!
|
||||
**Notes on legacy artifacts:**
|
||||
- The repository contains a legacy `Docusign_Configuration__c` custom object and older REST-helper classes in docs, but the active implementation uses the `dfsle` (DocuSign for Salesforce managed package) toolkit and `dfsle__EnvelopeConfiguration__c` records. The legacy custom setting/object is not required for the current Flow + Apex approach.
|
||||
|
||||
**Metadata:**
|
||||
- Custom Setting: `Docusign_Configuration__c` with `Account_Id__c` and `Base_URL__c` fields
|
||||
- All `.cls-meta.xml` files (API version 61.0)
|
||||
**All `.cls-meta.xml` files** use API version 60/61 as shown in each file.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -99,25 +98,18 @@ Pass Rate: 100%
|
|||
Code Coverage: 85%+
|
||||
```
|
||||
|
||||
### Step 7: Configure Docusign Credentials
|
||||
### Step 7: DocuSign Integration / Credentials (note)
|
||||
|
||||
1. **Create Named Credential** (see docs/deployment-guide.md section 3.1)
|
||||
- Setup → Named Credentials → New Named Credential
|
||||
- Name: `DocusignAPI`
|
||||
- URL: `https://na3.docusign.net/restapi/v2.1` (or your data center)
|
||||
- The current implementation uses the `dfsle` Apex Toolkit from the DocuSign for Salesforce managed package to send composite envelopes. That toolkit stores and references its templates/configuration using managed-package objects such as `dfsle__EnvelopeConfiguration__c`.
|
||||
|
||||
2. **Configure Custom Settings**
|
||||
- Setup → Custom Settings → Docusign Configuration → Manage
|
||||
- Click "New" (organization-wide default)
|
||||
- Account Id: `{Your Docusign Account GUID}`
|
||||
- Base URL: `callout:DocusignAPI`
|
||||
- Save
|
||||
- You do NOT need to create the legacy Named Credential / custom setting (`Docusign_Configuration__c`) for the dfsle-based flow. Ensure the `DocuSign for Salesforce (dfsle)` managed package is installed and its envelope configurations/templates exist in Production.
|
||||
|
||||
3. **Add Remote Site Settings**
|
||||
- Setup → Remote Site Settings → New Remote Site
|
||||
- Name: `Docusign_API`
|
||||
- URL: `https://na3.docusign.net`
|
||||
- Active: ✓
|
||||
- If you intentionally plan to use the older REST-based helper classes (legacy), then follow the steps below **only**:
|
||||
1. Create a Named Credential (Setup → Named Credentials → New)
|
||||
- Example name: `DocusignAPI`
|
||||
- Example URL: `https://na3.docusign.net/restapi/v2.1`
|
||||
2. (Optional legacy) Configure `Docusign_Configuration__c` custom setting/object with `Account_Id__c` and `Base_URL__c` if your older REST helpers require it.
|
||||
3. Add Remote Site or appropriate callout permissions for external DocuSign endpoints.
|
||||
|
||||
### Step 8: Update Screen Flow
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
Change Set Checklist — Docusign Composite Envelope Builder
|
||||
|
||||
Add these components to your Outbound Change Set in the Sandbox:
|
||||
|
||||
- Flow
|
||||
- Flow (component type: `Flow` / `Flow Definition`)
|
||||
- `DocuSign_Envelope_Templates` (use the V4 Flow version)
|
||||
|
||||
- Apex Classes (component type: `Apex Class`)
|
||||
- DocusignCompositeEnvelopeBuilder
|
||||
- DocusignEnvelopeRequest
|
||||
- DocusignEnvelopeRequestHandler
|
||||
- DocusignEnvelopeResult
|
||||
- DocusignCompositeEnvelopeBuilderTest (test class)
|
||||
- DocusignEnvelopeRequestHandlerTest (test class)
|
||||
|
||||
- Apex Triggers: None (this package does not include any triggers)
|
||||
|
||||
- Custom Objects / Fields: None required
|
||||
- Note: The repository contains a leftover `Docusign_Configuration__c` object with `Account_Id__c` and `Base_URL__c`, but the current implementation uses the `dfsle` managed-package objects (`dfsle__EnvelopeConfiguration__c`) and does not reference `Docusign_Configuration__c`. Do not include `Docusign_Configuration__c` unless you explicitly want to preserve it.
|
||||
|
||||
Post-deploy checklist in Production:
|
||||
- Assign permission sets (if any) to users.
|
||||
- Test Flow run via the relevant record page.
|
||||
- Run Apex test classes and verify overall code coverage.
|
||||
- Verify dfsle templates are present in Production (managed-package data/configuration).
|
||||
|
|
@ -132,11 +132,12 @@ sf apex run test --class-names DocusignCompositeEnvelopeBuilderTest --wait 10 --
|
|||
# Test the request handler
|
||||
sf apex run test --class-names DocusignEnvelopeRequestHandlerTest --wait 10 --result-format human --code-coverage --target-org dev-org
|
||||
|
||||
# Test the API service
|
||||
sf apex run test --class-names DocusignAPIServiceTest --wait 10 --result-format human --code-coverage --target-org dev-org
|
||||
|
||||
# Test the credentials helper
|
||||
sf apex run test --class-names DocusignCredentialsTest --wait 10 --result-format human --code-coverage --target-org dev-org
|
||||
# Legacy REST-helper tests (optional)
|
||||
# The REST-based `DocusignAPIService` and `DocusignCredentials` tests are part of a
|
||||
# legacy implementation and are not required for the current dfsle-based Flow.
|
||||
# Run these only if you have intentionally included the older REST helper classes in your package:
|
||||
# sf apex run test --class-names DocusignAPIServiceTest --wait 10 --result-format human --code-coverage --target-org dev-org
|
||||
# sf apex run test --class-names DocusignCredentialsTest --wait 10 --result-format human --code-coverage --target-org dev-org
|
||||
```
|
||||
|
||||
PowerShell:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,88 @@
|
|||
# Deploying Flow and APEX Code to Salesforce Production
|
||||
|
||||
This guide provides step-by-step instructions for moving your Flow and APEX code from a sandbox to a Salesforce production organization **using Salesforce's built-in tools** (no local source code required).
|
||||
|
||||
## Prerequisites
|
||||
- You have access to both the sandbox and production Salesforce orgs.
|
||||
- You have System Administrator permissions in both orgs.
|
||||
- The Flow and APEX code are already deployed and tested in the sandbox.
|
||||
|
||||
---
|
||||
|
||||
## 1. Use Change Sets to Deploy Metadata
|
||||
|
||||
### A. Create an Outbound Change Set in Sandbox
|
||||
1. **Login to your Sandbox**.
|
||||
2. Go to **Setup** > **Change Sets** > **Outbound Change Sets**.
|
||||
3. Click **New** to create a new change set (e.g., "Docusign Composite Envelope Builder").
|
||||
4. Add a description for clarity.
|
||||
5. Click **Save**.
|
||||
|
||||
### B. Add Components to the Change Set
|
||||
1. In the change set, click **Add** under "Change Set Components".
|
||||
2. **Add the Flow(s):**
|
||||
- Component Type: `Flow Definition` or `Flow`.
|
||||
- Select the active Flow version for the application. For this repository the Flow to include is `Docusign_Envelope_Templates_V4`.
|
||||
3. **Add APEX Classes:**
|
||||
- Component Type: `Apex Class`.
|
||||
- Include the following Apex classes from the package:
|
||||
- `DocusignCompositeEnvelopeBuilder`
|
||||
- `DocusignEnvelopeRequest`
|
||||
- `DocusignEnvelopeRequestHandler`
|
||||
- `DocusignEnvelopeResult`
|
||||
- Also include associated test classes:
|
||||
- `DocusignCompositeEnvelopeBuilderTest`
|
||||
- `DocusignEnvelopeRequestHandlerTest`
|
||||
4. **Apex Triggers:**
|
||||
- This package does not include any Apex Triggers. There are no trigger files in `force-app/main/default/triggers` to add to the change set.
|
||||
5. **Custom Objects / Fields:**
|
||||
- There are no custom objects or custom fields that must be deployed for the current implementation. The repository does contain a `Docusign_Configuration__c` object (with `Account_Id__c` and `Base_URL__c`) in `force-app/main/default/objects`, but it is not referenced by the APEX classes or the Flow (the implementation uses the `dfsle` toolkit and `dfsle__EnvelopeConfiguration__c` templates). You do not need to include `Docusign_Configuration__c` or its fields in the change set unless you intentionally want to preserve that leftover metadata.
|
||||
6. Click **Add to Change Set** after each selection.
|
||||
|
||||
### C. Add Profiles/Permission Sets (Optional)
|
||||
- If your app uses custom permissions, add the relevant profiles or permission sets.
|
||||
|
||||
### D. Upload the Change Set to Production
|
||||
1. In the change set, click **Upload**.
|
||||
2. Select your production org as the target.
|
||||
3. Click **Upload**.
|
||||
|
||||
---
|
||||
|
||||
## 2. Deploy the Change Set in Production
|
||||
|
||||
### A. Login to Production
|
||||
1. **Login to your Production Salesforce org**.
|
||||
2. Go to **Setup** > **Change Sets** > **Inbound Change Sets**.
|
||||
|
||||
### B. Validate and Deploy
|
||||
1. Find the uploaded change set.
|
||||
2. Click the change set name to review components.
|
||||
3. Click **Validate** to run tests and check for errors.
|
||||
4. If validation passes, click **Deploy**.
|
||||
5. Monitor deployment status and resolve any errors if needed.
|
||||
|
||||
---
|
||||
|
||||
## 3. Post-Deployment Steps
|
||||
- **Assign Permission Sets/Profiles** to users as needed.
|
||||
- **Test the Flow and APEX functionality** in production.
|
||||
- **Review Custom Settings/Metadata** for environment-specific values (e.g., API keys, URLs).
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
- If deployment fails, review error messages for missing dependencies or test failures.
|
||||
- Ensure all referenced components (objects, fields, flows, classes) are included in the change set.
|
||||
- For large or complex deployments, consider deploying in smaller batches.
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
- [Salesforce Change Sets Documentation](https://help.salesforce.com/s/articleView?id=sf.changesets.htm)
|
||||
- [Deploy Flows with Change Sets](https://help.salesforce.com/s/articleView?id=sf.flow_deploy.htm)
|
||||
- [Deploy Apex Code with Change Sets](https://help.salesforce.com/s/articleView?id=sf.code_deploy_changeset.htm)
|
||||
|
||||
---
|
||||
|
||||
**Contact your Salesforce administrator or partner for additional support if needed.**
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
%PDF-1.4
|
||||
%“Œ‹ž ReportLab Generated PDF document (opensource)
|
||||
1 0 obj
|
||||
<<
|
||||
/F1 2 0 R /F2 3 0 R
|
||||
>>
|
||||
endobj
|
||||
2 0 obj
|
||||
<<
|
||||
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
|
||||
>>
|
||||
endobj
|
||||
3 0 obj
|
||||
<<
|
||||
/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font
|
||||
>>
|
||||
endobj
|
||||
4 0 obj
|
||||
<<
|
||||
/Contents 9 0 R /MediaBox [ 0 0 612 792 ] /Parent 8 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
5 0 obj
|
||||
<<
|
||||
/Contents 10 0 R /MediaBox [ 0 0 612 792 ] /Parent 8 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
6 0 obj
|
||||
<<
|
||||
/PageMode /UseNone /Pages 8 0 R /Type /Catalog
|
||||
>>
|
||||
endobj
|
||||
7 0 obj
|
||||
<<
|
||||
/Author (anonymous) /CreationDate (D:20260330153955-04'00') /Creator (anonymous) /Keywords () /ModDate (D:20260330153955-04'00') /Producer (ReportLab PDF Library - \(opensource\))
|
||||
/Subject (unspecified) /Title (untitled) /Trapped /False
|
||||
>>
|
||||
endobj
|
||||
8 0 obj
|
||||
<<
|
||||
/Count 2 /Kids [ 4 0 R 5 0 R ] /Type /Pages
|
||||
>>
|
||||
endobj
|
||||
9 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1874
|
||||
>>
|
||||
stream
|
||||
Gatm<D/\/e&H88.EQFIm7\a(oALIF1^e(#t5dqMg4)BEJS#.T</L>/-PgoU'420o)M&f7Gco+qh'CY@7Si\"R6Qc@'jsNk'DJ!j5kCZ!L=Tn`F)o:kc]Uta^o`=hhH@=^uj*KO*>6H1<%.X2tcJGgbMf"ueiJrgd+DD,1cgUme>7iX3"l'd:<$W`#k:lc;Zeg_Er4i]aAtPn*$_#*[D&@UchnL60nGMn$0./qGStp[jn^TKB&-Q(sM8T9`R9e]\2e_?^CUFc"G[c;nD.3\7,/u`#.3@tWJ]_,=Z.`OJ:P_r79,k#89G%oE66N_2OO):H)N@:F"NXmd`oDW&6/d/@nO(2a)?B%V*'>A)R\]I<`;HOb/!P3^S,T3k<Q%#7Lf<(tE7O9jr%&T[SHjT'RL;_I.*M\iRA_G/]8.a@kNC^N[uEpJ#0=&n[2[8^?STaJ7Ol]Ol:N\WjiGE=s%%$W@,dUOPK@\r`oG5*02]N>4>7nB18QfL/!4`mh_6H=dM+WbBs<:*W>CX:#VW^!G(@7E?u8quGeZ[A-85mm3/pe%,Y(.49I@#B7\H[_'Dt^99dVHDE^hiQB8?>?`.)#I&oJjL9nS/>"9jqJUK2,hCGWiAn^1<(&Em#sR]t,'.Lp2K(lfSZ^'A![KA-i,f*EMB%E#WJ_/CkP+j!;GXTnd/N!l6fBQ^Pcq)2\&?!j'lV]OV;E]G>2_P\SbM#r7/"4uJe>N"3hH][^D<PLr#fD?uIVMj*@k1oTT>r=GW:E8d)%t5+:_d3j`0/Dlg5N?g;#Igu`;g!cR^q(O1LchQlET>s0,J(YrW'_:tp'B/o;2FE5N-T4-%@k-G'RT.,`UbY&K^N195(-gDA.3@[_hLjVmc]Ub65l,kIcOqZ@NAiJ-n=9$1_G/%:ekPkD`dDM[*/\rZM2umG+a"\I"'b]DetL>Js<+?:3<bl]_jFg)dX'?demMM5u#K@`5H8@_'".S$msr?;$D)mVd49]3lMUl]>*m<]@d$Tc-P;@H<p9GeGa4/XNdct@CFG5>?I=0qMn"scF#'dfXU>.Z?'nLRn0's4l_L`-@LJ9g4S_Q%ns!$Rc_456Scm\iPD'f`+_du;^4VH/,ZbRifs!XgbSqFhc2\p*m:kM(1j0kAh;%`eu;jN1$Y@DX,(3+X;t(s;2V=tM"TL7lgH-CR^!scJ+m4ED-T/t%i0<*6mOa4Tk?dRS_EGt7&n#J#lRNLltsdf#$&LR)Gt?PO^#6BP%<0<QX(hTFo/b"B2j.0SUSD;'W7#;:>6M-D77d\YBZH*,EcqCd&#,"hl>VjEuq`a\h]aq<E25Ug]WUnT(DTa<sl1<hM,n"*qn?&D0??\mE31%4$`aA1%_O"!BZaQkF%:\C[BF(gR6s%Jnm/`.sC:]6@hiV'm\&F\qMLDH%=KIBrCtFLBb@!;2j*P4gcoe?0Ph5@op6dhHVBQ:+nsM&X;a\gIX!A/Qj1Jin:uk[CQ-6\HWu)gHt#S*C$[?iH_B['(05K_dag,;lunD'==\I6+@Ku<+o>W'+3kpE_(-U<ij#6Pt*Y_Ur6bU^WZK'`T*40B^<rbI=l;Tjb@=rn7s;.1_/_RO[elPOQNsiJhC?-3'KRHq.@D)Z9kIRid@@O5(r'8I2cGJ&B,Dl^O4N]oCI]+?:_ljZ?ubT![WYPN>VE%Xbp#^#[pIWU:%;%MS5q!A'k<j`g?LhIXI"a'\Z0_0>g>#[PYa/q2Q%p#5d5gZF0-PWq#3tMKq5Ni6;>q%6kQ(b#66r2Y5GAj)u5=&g:2MPiClT2NHBos3:^"IJ@B6WT/nio.<(f=%)/m'na^U;bTA/X&@6;>i6/"M7Zi5Xj3q+a7[\hC<O?*!T:)16ofR%KPiIO4j%M.2giH`^Bn5D$3~>endstream
|
||||
endobj
|
||||
10 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1524
|
||||
>>
|
||||
stream
|
||||
Gatm;@8GoC&H0nXR#anYVG3MTkt-(H[_N6,eKO.Z:>2:?-?35X-?^(bYl+A;gIW<VckI$(F]0Z+hO?IaM$IPgo?@61!;NpOd?t62%uUk/^^X[k!N1hXF5>`"h.n&D"u)UdYUJGIgkFcFL!*)!/>mN18ZRG7aTRB7W[',[r)5*C7e5["+Y8)Z_Wa;sHu)(G54l:aI:-u*J=#,7qXB('VC:^fSS)mpl3^'b67o0iUm=Ue:j,^Y,9Ri<>#VA5:2^G?:c;iL<5"hFOcTC,_eu0f@lH6\S@J'rbQY7bo/5JWW^Qt8[!l_[BVF9OUaYpT:sXi:D?(=XLb+Q/a]\0B*66(Z3)=5TH@BDtoj&i)dmR?,bDl`u[fR`_YThVTWkBRmW1bnb@K&U+@rouHk5u7_%_0&li^3`T(n4M8f3PBX!uST=G"DSDAT^<Ol2G[s(uLo52]R1NaFhO*rLh#qO*kbGG23>14iKLR'i*dYTjnlk`!W8dIj]s/CuZe]l!R3!2ct#nY[B4`,ugdkIn,4/[+5/^ciB)YMV"]b^gFV.,XF=icM<HuGmY(2ftTX:^D6:&$f=M#;PGQNI'C#oqhR6"M0R%Mm/XOK1JN4]\Z]baXAdB0aMU=L:("#?XQV72GE@WT'V+_GI;B]r*'C@C<\DUP`i.9%<+=2*l6U5'HdE<hX%H@g"&=7H(L[9ci79)dUQn5V,N#j^S#6,n(NdY,?2,U';]':b*`eq#+;Ib]j\er)nltL=qh>Qa$ok)N]U(]W0PO0dYD.7q^mhcA?OEbrSo!Dt<j\*<p7BAWm!=^un)unr[%#"_7_9I4iQX:$3Q<jsL$eKjGVEmq#&>AEU/>,W2P+YF.'2ZpTVcQs$T+n5R'U&QR7&-(T)K+#bnE1D0SR6h9n.5em2]*c%oTF^ju?GN<p+X(6'W/E.lmGm'u:M*(m1dII`j4W[3\#ZOsV_iM(Y!\lC!RA/,'>6SAR"+O2^\_l9[*6?hWaf-b@;A\/OecM*En\0.KiV?n0bpk_Su?W'?2%Mq[jY/F(R$6\O?5__0E=rf"'$F_giN.=*D@P[oLECAj-Mq+BO3GtNA!m(B8YTXh\SY-G07r+S89jCZjiQ\YYfcS<Y&<Z$HQ9=bn"2@qf3EAg0*)5DJW5LJ_mGFBki49U73?p)PHI1@tf-pA.]KoK-:5Ht')14Wf>km[7/p$eo6e\lM)]9ad;":'1;d^F/]KHLS(TI>'TL+#0=jNG"+rLWQW#KGM4IG\hrF4Fl1hq%/5ZNa[<hWB):p3)AKDYi4ZfQo@'oCk]@TQDB@>3g+Pj8YDeek24uO3qW8o?ro[;dIW"8<-6;=H_-O=@[kIoM:mceii/4C9<A%YD%Cj@5NLGZfufmaQjTpiJsm[6h^=;K1WsZbOGpc]#K2$5JE08A4RK*9eF-?&kc^e;EBc3(Vbr>Jk-?k+'gq"Y^K\kZ_0$7/me0rrrkGu%`Z%EcaBsF4nk,H@%9eg&O""qGBOC^A,Y^WNA(%MT&q0]Z4rShc#/QjM\Zg%#D;_sDZ~>endstream
|
||||
endobj
|
||||
xref
|
||||
0 11
|
||||
0000000000 65535 f
|
||||
0000000061 00000 n
|
||||
0000000102 00000 n
|
||||
0000000209 00000 n
|
||||
0000000321 00000 n
|
||||
0000000514 00000 n
|
||||
0000000708 00000 n
|
||||
0000000776 00000 n
|
||||
0000001037 00000 n
|
||||
0000001102 00000 n
|
||||
0000003067 00000 n
|
||||
trailer
|
||||
<<
|
||||
/ID
|
||||
[<ee950e95b19116a21bdbd23f3647697f><ee950e95b19116a21bdbd23f3647697f>]
|
||||
% ReportLab generated PDF document -- digest (opensource)
|
||||
|
||||
/Info 7 0 R
|
||||
/Root 6 0 R
|
||||
/Size 11
|
||||
>>
|
||||
startxref
|
||||
4683
|
||||
%%EOF
|
||||
|
|
@ -211,12 +211,14 @@ sf project deploy start --target-org my-sandbox
|
|||
Deploying v60.0 metadata to my-sandbox using the v60.0 SOAP API
|
||||
Status: Succeeded
|
||||
Component Deployed:
|
||||
ApexClass DocusignCompositeEnvelopeBuilder
|
||||
ApexClass DocusignAPIService
|
||||
ApexClass DocusignCredentials
|
||||
ApexClass DocusignCompositeEnvelopeBuilderTest
|
||||
ApexClass DocusignAPIServiceTest
|
||||
ApexClass DocusignCredentialsTest
|
||||
ApexClass DocusignCompositeEnvelopeBuilder
|
||||
ApexClass DocusignEnvelopeRequest
|
||||
ApexClass DocusignEnvelopeRequestHandler
|
||||
ApexClass DocusignEnvelopeResult
|
||||
ApexClass DocusignCompositeEnvelopeBuilderTest
|
||||
ApexClass DocusignEnvelopeRequestHandlerTest
|
||||
|
||||
Note: The older `DocusignAPIService` / `DocusignCredentials` classes and their tests are part of a legacy REST-based implementation. The active implementation uses the `dfsle` toolkit and does not require those classes; include them only if you intentionally retain the legacy REST helpers.
|
||||
```
|
||||
|
||||
### 4.4 Run Unit Tests
|
||||
|
|
|
|||
|
|
@ -1,203 +1,232 @@
|
|||
# Design Document
|
||||
# Design — Composite Envelope Builder (Updated)
|
||||
|
||||
**Project**: Salesforce Composite Envelope Builder
|
||||
**Version**: 1.2
|
||||
**Date**: February 23, 2026 (updated March 15, 2026)
|
||||
**Author**: Paul Huliganga
|
||||
**Project**: Salesforce Composite Envelope Builder
|
||||
**Version**: 2.0
|
||||
**Date**: 2026-03-25
|
||||
**Author**: (auto-generated; update author/owner as needed)
|
||||
|
||||
---
|
||||
|
||||
## 1. Architecture Overview
|
||||
## 1. Summary
|
||||
|
||||
### 1.1 System Context
|
||||
This document describes the current implementation of the Composite Envelope Builder: how Screen Flows, Apex, and the dfsle toolkit collaborate to build and send a single DocuSign envelope composed from multiple DocuSign templates. It reflects the latest code and metadata in the repository (March 2026), including:
|
||||
|
||||
```
|
||||
┌──────────────────┐
|
||||
│ Salesforce User │
|
||||
└────────┬─────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Salesforce Platform │
|
||||
│ ┌──────────────┐ ┌───────────────┐ │
|
||||
│ │ Screen Flow │───▶│ Apex Class │ │
|
||||
│ │ (Template │ │ (Composite │ │
|
||||
│ │ Selection) │ │ Builder) │ │
|
||||
│ └──────────────┘ └───────┬───────┘ │
|
||||
│ │ │
|
||||
└──────────────────────────────┼───────────┘
|
||||
│
|
||||
▼ HTTPS/REST API
|
||||
┌──────────────────────┐
|
||||
│ Docusign REST API │
|
||||
│ (Composite │
|
||||
│ Templates) │
|
||||
└──────────────────────┘
|
||||
```
|
||||
- Flow V3 (existing, email-only path) and Flow V4 (new, SMS collection path when recipient email is blank)
|
||||
- Apex invocable: `DocusignCompositeEnvelopeBuilder.sendCompositeEnvelope` and supporting classes
|
||||
- SMS delivery support for recipients without an email (via `dfsle.Recipient.withSmsDelivery()`)
|
||||
- Multi-copy support for the "Authorization to Release Information" template (up to 5 copies)
|
||||
- Email subject & body composition rules: prefix `Docusign: `, truncation, greeting/signoff handling and Spanish detection
|
||||
- Key constants: `SMS_PLACEHOLDER_EMAIL`, `MULTI_COPY_TEMPLATE_NAME`
|
||||
|
||||
### 1.2 Component Architecture
|
||||
This is the canonical design doc for developers, release engineers, and reviewers.
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────────────────┐
|
||||
│ Salesforce Org │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────┐ │
|
||||
│ │ Presentation Layer (Screen Flow) │ │
|
||||
│ │ - Language selection │ │
|
||||
│ │ - Template display (checkbox collection) │ │
|
||||
│ │ - Success/error messaging │ │
|
||||
│ └──────────────────┬──────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ┌─────────────────▼──────────────────────────────┐ │
|
||||
│ │ Business Logic Layer (Apex) │ │
|
||||
│ │ │ │
|
||||
│ │ ┌─────────────────────────────────────────┐ │ │
|
||||
│ │ │ DocusignCompositeEnvelopeBuilder │ │ │
|
||||
│ │ │ - @InvocableMethod entry point │ │ │
|
||||
│ │ │ - Input validation │ │ │
|
||||
│ │ │ - Multi-copy template expansion │ │ │
|
||||
│ │ │ - SMS delivery (withSmsDelivery) │ │ │
|
||||
│ │ │ - Composite JSON construction │ │ │
|
||||
│ │ │ - Envelope ID return │ │ │
|
||||
│ │ └──────────────────┬──────────────────────┘ │ │
|
||||
│ │ │ │ │
|
||||
│ │ ┌─────────────────▼──────────────────────┐ │ │
|
||||
│ │ │ DocusignAPIService │ │ │
|
||||
│ │ │ - API authentication │ │ │
|
||||
│ │ │ - HTTP callout construction │ │ │
|
||||
│ │ │ - Response parsing │ │ │
|
||||
│ │ │ - Error handling │ │ │
|
||||
│ │ └──────────────────┬──────────────────────┘ │ │
|
||||
│ │ │ │ │
|
||||
│ │ ┌─────────────────▼──────────────────────┐ │ │
|
||||
│ │ │ DocusignCredentials │ │ │
|
||||
│ │ │ - Credential retrieval │ │ │
|
||||
│ │ │ - Token management │ │ │
|
||||
│ │ └─────────────────────────────────────────┘ │ │
|
||||
│ └──────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌──────────────────────────────────────────────┐ │
|
||||
│ │ Data Layer │ │
|
||||
│ │ - Named Credential (Docusign API creds) │ │
|
||||
│ │ - Custom Settings (configuration) │ │
|
||||
│ └──────────────────────────────────────────────┘ │
|
||||
└───────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼ HTTPS REST API
|
||||
┌──────────────────────┐
|
||||
│ Docusign Platform │
|
||||
└──────────────────────┘
|
||||
---
|
||||
|
||||
## 2. Architecture Overview
|
||||
|
||||
High-level flow:
|
||||
|
||||
- User launches Screen Flow (Flow V3 or V4)
|
||||
- User selects language (English/Spanish) and templates
|
||||
- Flow optionally collects SMS phone when recipient email is blank (V4)
|
||||
- Flow invokes Apex action `Send Composite Docusign Envelope` with inputs
|
||||
- Apex builds compound envelope JSON using selected templates (expands multi-copy templates as configured)
|
||||
- Apex uses dfsle toolkit to send envelope; returns envelopeId and success/error
|
||||
|
||||
Components:
|
||||
|
||||
- Screen Flows: `Docusign_Envelope_Templates_V3` (email path), `Docusign_Envelope_Templates_V4` (collect SMS phone when needed)
|
||||
- Apex:
|
||||
- `DocusignCompositeEnvelopeBuilder.cls` (invocable entrypoint + implementation)
|
||||
- `DocusignEnvelopeRequest.cls` (invocable request contract)
|
||||
- `DocusignEnvelopeResult.cls` (invocable result contract)
|
||||
- `DocusignAPIService.cls`, `DocusignCredentials.cls` (service & credential management)
|
||||
- DocuSign: Composite Templates API (via dfsle toolkit integration)
|
||||
|
||||
---
|
||||
|
||||
## 3. Flow Behavior (V3 and V4)
|
||||
|
||||
### 3.1 Shared behavior (V3 & V4)
|
||||
|
||||
- Language selection screen: user selects `English` or `Spanish`.
|
||||
- Template selection screen: multi-select list of templates filtered by selected language.
|
||||
- After selection, a loop scans selected templates to detect the multi-copy template ("Authorization to Release Information").
|
||||
- If detected, a copies screen appears (radio 1–5) that sets `authReleaseFormCopies`.
|
||||
- Flow gathers `compositeTemplateIds` (selected template IDs) and passes them to Apex action.
|
||||
|
||||
### 3.2 Flow V3
|
||||
|
||||
- Used when standard email delivery is expected.
|
||||
- No SMS collection screen is present.
|
||||
- Behavior is unchanged from previous releases except choices include 1–5 copies.
|
||||
|
||||
### 3.3 Flow V4 (SMS-aware)
|
||||
|
||||
- Added pre-send recipient lookup path:
|
||||
- `Get_Records` (Client_Case__c) → `Get_Recipient_Contact` (Contact lookup)
|
||||
- `Is_Recipient_Email_Blank` decision checks Contact.Email
|
||||
- If email is blank → `SMS_Phone_Screen` (field `smsPhoneInput` required, E.164 format recommended) → `Store_SMS_Phone` assignment assigns to `recipientSmsPhone` variable → continue
|
||||
- If email present → skip SMS_Phone_Screen and continue
|
||||
- The `recipientSmsPhone` variable is passed into the Apex action; when non-blank the Apex sets up SMS delivery for Docusign Recipient #1.
|
||||
- Flow element names to reference in code and docs: `SMS_Phone_Screen`, `smsPhoneInput`, `Store_SMS_Phone`, `recipientSmsPhone`.
|
||||
|
||||
Note: Flow metadata must keep element blocks contiguous (assignments, decisions, screens, etc.) to avoid deployment errors. The committed `Docusign_Envelope_Templates_V4.flow-meta.xml` already follows these rules.
|
||||
|
||||
---
|
||||
|
||||
## 4. Apex Implementation Details
|
||||
|
||||
### 4.1 Invocable contract (`DocusignEnvelopeRequest`)
|
||||
|
||||
Key input fields (invocable variables):
|
||||
- `List<String> templateIds` (required) — list of selected DocuSign template IDs
|
||||
- `String recordId` (required) — Salesforce Client Case Id
|
||||
- `String language` (optional) — e.g., `en`, `es`, `Spanish`, `Español` (language detection is normalized in Apex)
|
||||
- `String emailSubject` (optional) — optional override; Apex still prefixes with `Docusign: ` and truncates to 100 chars
|
||||
- `Integer authReleaseFormCopies` (optional) — 1–5; counts for Authorization template
|
||||
- `String recipientSmsPhone` (optional) — when provided, Apex will enable SMS delivery for Docusign Recipient #1
|
||||
|
||||
Outputs:
|
||||
- `String envelopeId`
|
||||
- `Boolean success`
|
||||
- `String errorMessage`
|
||||
|
||||
### 4.2 Main behavior (`DocusignCompositeEnvelopeBuilder.sendCompositeEnvelope`)
|
||||
|
||||
1. Validate inputs; throw friendly errors if invalid.
|
||||
2. Expand multi-copy templates:
|
||||
- Find template IDs whose `Name` contains the constant `MULTI_COPY_TEMPLATE_NAME`.
|
||||
- If `authReleaseFormCopies` > 1, append (copies - 1) additional entries for each matched template ID.
|
||||
- Cap copies to 5 (Math.min(..., 5)).
|
||||
3. Build document list (documents = fromTemplate(templateId, label)) and label duplicates as ` (Copy N)` to preserve uniqueness.
|
||||
4. Build deduplicated display names for subject/body (merge repeated templates into count markers like `X (3)`).
|
||||
5. Compose envelope subject & body (see section 5 for rules).
|
||||
6. Resolve recipients via `resolveRecipients(recordId, recipientSmsPhone)`:
|
||||
- Query Client_Case__c for lookup fields
|
||||
- For Docusign Recipient #1: if linked Contact has no Email and `recipientSmsPhone` is provided, set recipientEmail to `SMS_PLACEHOLDER_EMAIL` and call `recipient.withSmsDelivery(smsPhone)`.
|
||||
7. Use dfsle toolkit to build envelope with documents and recipients and call send.
|
||||
8. Return `envelopeId` and `success`.
|
||||
|
||||
### 4.3 Important constants
|
||||
|
||||
- `SMS_PLACEHOLDER_EMAIL` (defined in `DocusignCompositeEnvelopeBuilder.cls`): placeholder email used when recipient has no email; required by DocuSign API even for SMS deliveries.
|
||||
- `MULTI_COPY_TEMPLATE_NAME` (defined in same class): base name used to identify the Authorization to Release Information template (covers English + Spanish variations via LIKE).
|
||||
|
||||
### 4.4 i18n detection
|
||||
|
||||
- The code normalizes `req.language` to lowercase and treats as Spanish if any of the following are true:
|
||||
- `lang.startsWith('es')` (e.g. `es`, `es-CO`)
|
||||
- `lang.contains('spanish')`
|
||||
- `lang.contains('espa')` (to capture `español` / `espanol`)
|
||||
- On Spanish detection, Apex uses Spanish greeting/signoff; otherwise English is used.
|
||||
|
||||
---
|
||||
|
||||
## 5. Email Subject & Body Composition Rules
|
||||
|
||||
- Subject:
|
||||
- Prefix: `Docusign: ` is prepended to the subject to make the source explicit.
|
||||
- Truncation: After prefixing, subject is truncated to a maximum of 100 characters (Docusign requirement). The code uses `left(97) + '...'` if longer.
|
||||
- If user supplies `emailSubject`, prefix and truncation still apply.
|
||||
|
||||
- Body:
|
||||
- Built as: GREETING + (template bodies joined by DIVIDER) + SIGNOFF
|
||||
- DIVIDER: `"\n\n" + '─'.repeat(40) + "\n\n"` (visual separator)
|
||||
- Greeting/Sign-off: English default; Spanish strings used when language detection matches Spanish variants. Examples:
|
||||
- English greeting: `Hello,\n\nPlease complete the DocuSign signature request from Early Intervention Colorado.\n\n`
|
||||
- Spanish greeting: `Hola,\n\nPor favor, firme la solicitud de DocuSign de parte de Intervención Temprana Colorado.\n\n`
|
||||
|
||||
---
|
||||
|
||||
## 6. Multi-copy Behavior
|
||||
|
||||
- Single constant drives detection: `MULTI_COPY_TEMPLATE_NAME = 'Authorization to Release Information'`.
|
||||
- Flow shows a copies screen when the template is present; operator selects 1–5 copies.
|
||||
- Apex expands the templateId list before building documents to include multiple entries; duplicates are intentionally kept so each copy becomes a separate document in the envelope.
|
||||
- Document labels: duplicates are labeled with ` (Copy N)` so they appear distinct in DocuSign Status.
|
||||
|
||||
---
|
||||
|
||||
## 7. SMS Delivery Details
|
||||
|
||||
- Trigger: Flow V4 routes to `SMS_Phone_Screen` if the Contact linked by `Docusign_Recipient_1__c` has empty `Email`.
|
||||
- The screen collects `smsPhoneInput` which is assigned to `recipientSmsPhone` variable and passed to Apex.
|
||||
- In Apex, when `recipientSmsPhone` is non-blank for Docusign Recipient #1:
|
||||
- If recipient record has no email, set `recipientEmail = SMS_PLACEHOLDER_EMAIL`.
|
||||
- Call `recipient = recipient.withSmsDelivery(smsPhone)` to enable SMS delivery via dfsle toolkit.
|
||||
- Service Coordinator recipient always uses email delivery (unchanged).
|
||||
- Important setup requirement (documented): `Docusign_Recipient_1__c` on the Client Case must be populated with a Contact. The Contact must have `Name` and (for SMS flow) may have blank `Email`. If the Contact is null, the recipient will be omitted and envelope will be missing that signer.
|
||||
|
||||
---
|
||||
|
||||
## 8. Flows & Metadata Notes
|
||||
|
||||
- Flow element naming conventions are important (screen fields bind by their `name`). For the SMS flow, the screen field is `smsPhoneInput` and the assignment must use `<elementReference>smsPhoneInput</elementReference>` (not `ScreenName.smsPhoneInput`).
|
||||
- Flow metadata must keep element blocks contiguous (screens, assignments, recordLookups, decisions, loops, choices) to avoid Salesforce Flow XML validation failures. The repo's V4 flow file was normalized accordingly.
|
||||
|
||||
---
|
||||
|
||||
## 9. Tests, Validation & CI
|
||||
|
||||
- Unit tests: ensure `DocusignCompositeEnvelopeBuilderTest` covers:
|
||||
- English and Spanish subject/body composition
|
||||
- Multi-copy expansion with 1–5 copies
|
||||
- SMS flow path: when Contact.Email blank + recipientSmsPhone provided, mailbox placeholder used and `withSmsDelivery` applied
|
||||
- Error branches and API failures
|
||||
- CI: add a PR validation job that does:
|
||||
- sfdx auth to a validation org
|
||||
- For changed flow or Apex files, run `sfdx force:source:deploy --checkonly` for those components
|
||||
- Fail PR if check-only returns errors
|
||||
- Pre-deploy: run check-only deploy to target org before actual deploy. Example local command:
|
||||
|
||||
```bash
|
||||
sfdx force:source:deploy -p force-app/main/default/flows/Docusign_Envelope_Templates_V4.flow-meta.xml,force-app/main/default/classes/DocusignCompositeEnvelopeBuilder.cls -u dev-org --checkonly --testlevel NoTestRun
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Detailed Component Design
|
||||
## 10. Edge Cases and Known Gotchas
|
||||
|
||||
### 2.1 DocusignCompositeEnvelopeBuilder (Main Class)
|
||||
|
||||
**Purpose**: Invocable Apex class that combines multiple Docusign templates into a single envelope
|
||||
|
||||
**Responsibilities**:
|
||||
- Receive template IDs from Screen Flow
|
||||
- Validate inputs
|
||||
- Expand multi-copy templates (e.g. Authorization to Release Information)
|
||||
- Construct composite template JSON
|
||||
- Delegate API call to service class
|
||||
- Return envelope ID to Flow
|
||||
|
||||
**Methods**:
|
||||
|
||||
```apex
|
||||
// Flow-invocable entry point
|
||||
@InvocableMethod(
|
||||
label='Send Composite Docusign Envelope'
|
||||
description='Combines multiple templates into one envelope'
|
||||
)
|
||||
public static List<Result> sendCompositeEnvelope(List<Request> requests)
|
||||
|
||||
// Private helper methods
|
||||
private static String buildCompositeEnvelopeJSON(
|
||||
List<String> templateIds,
|
||||
String recordId,
|
||||
String language,
|
||||
Map<String, String> customFields
|
||||
)
|
||||
|
||||
private static void validateInputs(Request req)
|
||||
|
||||
private static List<String> sortTemplatesAlphabetically(List<String> templateIds)
|
||||
```
|
||||
|
||||
**Inner Classes**:
|
||||
|
||||
```apex
|
||||
public class Request {
|
||||
@InvocableVariable(required=true label='Template IDs')
|
||||
public List<String> templateIds;
|
||||
|
||||
@InvocableVariable(required=true label='Salesforce Record ID')
|
||||
public String recordId;
|
||||
|
||||
@InvocableVariable(required=false label='Language')
|
||||
public String language; // 'en' or 'es'
|
||||
|
||||
@InvocableVariable(required=false label='Email Subject')
|
||||
public String emailSubject;
|
||||
|
||||
@InvocableVariable(required=false label='Authorization to Release Form Copies')
|
||||
public Integer authReleaseFormCopies; // 1–5; only used when that template is selected
|
||||
|
||||
@InvocableVariable(required=false label='Recipient SMS Phone')
|
||||
public String recipientSmsPhone; // E.164 preferred (+15551234567); triggers SMS delivery for Docusign Recipient #1
|
||||
|
||||
@InvocableVariable(required=false label='Custom Fields')
|
||||
public Map<String, String> customFields; // For merge fields
|
||||
}
|
||||
|
||||
public class Result {
|
||||
@InvocableVariable(label='Envelope ID')
|
||||
public String envelopeId;
|
||||
|
||||
@InvocableVariable(label='Success')
|
||||
public Boolean success;
|
||||
|
||||
@InvocableVariable(label='Error Message')
|
||||
public String errorMessage;
|
||||
}
|
||||
```
|
||||
- If operator edits a flow in the org and does not retrieve+commit the changes, repo and org diverge. Use mdapi `retrieve` or `sf project retrieve` to pull the authoritative version and commit immediately.
|
||||
- Flow Builder sometimes reorders or normalizes XML: prefer re-checking contiguity of element blocks after Flow Builder saves.
|
||||
- Ensure the Flow variable `recipientSmsPhone` is passed to Apex action and that the assignment uses `smsPhoneInput` as elementReference.
|
||||
- The DocuSign API requires an email field on every recipient even when SMS is used; `SMS_PLACEHOLDER_EMAIL` is central and must be kept updated in Apex if changed.
|
||||
|
||||
---
|
||||
|
||||
### 2.2 DocusignAPIService (Service Class)
|
||||
## 11. Deployment Checklist
|
||||
|
||||
**Purpose**: Handles all Docusign REST API interactions
|
||||
1. Confirm `force-app/main/default/flows/Docusign_Envelope_Templates_V4.flow-meta.xml` matches the active version in org (if you intend to deploy active draft)
|
||||
2. Confirm `force-app/main/default/classes/DocusignCompositeEnvelopeBuilder.cls` and related classes/tests are committed
|
||||
3. Run `sfdx force:source:deploy --checkonly` for affected components against a validation org
|
||||
4. Run Apex tests (or allow CI to run tests during deploy)
|
||||
5. Deploy to sandbox, perform a manual Flow Builder inspection of V4
|
||||
6. Activate the desired Flow version if runtime activation is needed
|
||||
|
||||
**Responsibilities**:
|
||||
- Construct HTTP requests
|
||||
- Make callouts to Docusign API
|
||||
- Parse responses
|
||||
- Handle errors and retries
|
||||
- Log API interactions
|
||||
---
|
||||
|
||||
**Methods**:
|
||||
## 12. Change Log (high level)
|
||||
|
||||
```apex
|
||||
public static String createCompositeEnvelope(
|
||||
String envelopeJSON,
|
||||
DocusignCredentials creds
|
||||
)
|
||||
- 2.0 (2026-03-25): Updated design to reflect:
|
||||
- Flow V4 (SMS path) retrieval & Active flow committed
|
||||
- Spanish greeting/signoff and broadened language detection
|
||||
- Envelope subject prefix `Docusign: ` and truncation to 100 chars
|
||||
- Increased Authorization multi-copy support to 5 copies
|
||||
- Flow element and metadata normalization
|
||||
|
||||
public static HttpResponse callDocusignAPI(
|
||||
String endpoint,
|
||||
String method,
|
||||
String body,
|
||||
Map<String, String> headers
|
||||
)
|
||||
---
|
||||
|
||||
public static String parseEnvelopeId(HttpResponse response)
|
||||
## 13. Contact & Ownership
|
||||
|
||||
- Primary developer: Paul Huliganga (update as appropriate)
|
||||
- Repo: `composite-envelope-builder`
|
||||
- For deployment questions: refer to `docs/DEPLOYMENT_AND_TESTING.md`
|
||||
|
||||
---
|
||||
|
||||
*This document was generated to represent the repository state as of March 25, 2026. Update the change log and ownership fields when you make future changes.*
|
||||
|
||||
private static void logAPICall(
|
||||
HttpRequest req,
|
||||
HttpResponse res,
|
||||
Long durationMs
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,201 @@
|
|||
# Design Document (previous)
|
||||
|
||||
**Project**: Salesforce Composite Envelope Builder
|
||||
**Version**: 1.2
|
||||
**Date**: February 23, 2026 (updated March 15, 2026)
|
||||
**Author**: Paul Huliganga
|
||||
|
||||
---
|
||||
|
||||
## 1. Architecture Overview
|
||||
|
||||
### 1.1 System Context
|
||||
|
||||
```
|
||||
┌──────────────────┐
|
||||
│ Salesforce User │
|
||||
└────────┬─────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Salesforce Platform │
|
||||
│ ┌──────────────┐ ┌───────────────┐ │
|
||||
│ │ Screen Flow │───▶│ Apex Class │ │
|
||||
│ │ (Template │ │ (Composite │ │
|
||||
│ │ Selection) │ │ Builder) │ │
|
||||
│ └──────────────┘ └───────┬───────┘ │
|
||||
│ │ │
|
||||
└──────────────────────────────┼───────────┘
|
||||
│
|
||||
▼ HTTPS/REST API
|
||||
┌──────────────────────┐
|
||||
│ Docusign REST API │
|
||||
│ (Composite │
|
||||
│ Templates) │
|
||||
└──────────────────────┘
|
||||
```
|
||||
|
||||
### 1.2 Component Architecture
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────────────────┐
|
||||
│ Salesforce Org │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────┐ │
|
||||
│ │ Presentation Layer (Screen Flow) │ │
|
||||
│ │ - Language selection │ │
|
||||
│ │ - Template display (checkbox collection) │ │
|
||||
│ │ - Success/error messaging │ │
|
||||
│ └──────────────────┬──────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ┌─────────────────▼──────────────────────────────┐ │
|
||||
│ │ Business Logic Layer (Apex) │ │
|
||||
│ │ │ │
|
||||
│ │ ┌─────────────────────────────────────────┐ │ │
|
||||
│ │ │ DocusignCompositeEnvelopeBuilder │ │ │
|
||||
│ │ │ - @InvocableMethod entry point │ │ │
|
||||
│ │ │ - Input validation │ │ │
|
||||
│ │ │ - Multi-copy template expansion │ │ │
|
||||
│ │ │ - SMS delivery (withSmsDelivery) │ │ │
|
||||
│ │ │ - Composite JSON construction │ │ │
|
||||
│ │ │ - Envelope ID return │ │ │
|
||||
│ │ └──────────────────┬──────────────────────┘ │ │
|
||||
│ │ │ │ │
|
||||
│ │ ┌─────────────────▼──────────────────────┐ │ │
|
||||
│ │ │ DocusignAPIService │ │ │
|
||||
│ │ │ - API authentication │ │ │
|
||||
│ │ │ - HTTP callout construction │ │ │
|
||||
│ │ │ - Response parsing │ │ │
|
||||
│ │ │ - Error handling │ │ │
|
||||
│ │ └──────────────────┬──────────────────────┘ │ │
|
||||
│ │ │ │ │
|
||||
│ │ ┌─────────────────▼──────────────────────┐ │ │
|
||||
│ │ │ DocusignCredentials │ │ │
|
||||
│ │ │ - Credential retrieval │ │ │
|
||||
│ │ │ - Token management │ │ │
|
||||
│ │ └─────────────────────────────────────────┘ │ │
|
||||
│ └──────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌──────────────────────────────────────────────┐ │
|
||||
│ │ Data Layer │ │
|
||||
│ │ - Named Credential (Docusign API creds) │ │
|
||||
│ │ - Custom Settings (configuration) │ │
|
||||
│ └──────────────────────────────────────────────┘ │
|
||||
└───────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼ HTTPS REST API
|
||||
┌──────────────────────┐
|
||||
│ Docusign Platform │
|
||||
└──────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Detailed Component Design
|
||||
|
||||
### 2.1 DocusignCompositeEnvelopeBuilder (Main Class)
|
||||
|
||||
**Purpose**: Invocable Apex class that combines multiple Docusign templates into a single envelope
|
||||
|
||||
**Responsibilities**:
|
||||
- Receive template IDs from Screen Flow
|
||||
- Validate inputs
|
||||
- Expand multi-copy templates (e.g. Authorization to Release Information)
|
||||
- Construct composite template JSON
|
||||
- Delegate API call to service class
|
||||
- Return envelope ID to Flow
|
||||
|
||||
**Methods**:
|
||||
|
||||
```apex
|
||||
// Flow-invocable entry point
|
||||
@InvocableMethod(
|
||||
label='Send Composite Docusign Envelope'
|
||||
description='Combines multiple templates into one envelope'
|
||||
)
|
||||
public static List<Result> sendCompositeEnvelope(List<Request> requests)
|
||||
|
||||
// Private helper methods
|
||||
private static String buildCompositeEnvelopeJSON(
|
||||
List<String> templateIds,
|
||||
String recordId,
|
||||
String language,
|
||||
Map<String, String> customFields
|
||||
)
|
||||
|
||||
private static void validateInputs(Request req)
|
||||
|
||||
private static List<String> sortTemplatesAlphabetically(List<String> templateIds)
|
||||
```
|
||||
|
||||
**Inner Classes**:
|
||||
|
||||
```apex
|
||||
public class Request {
|
||||
@InvocableVariable(required=true label='Template IDs')
|
||||
public List<String> templateIds;
|
||||
|
||||
@InvocableVariable(required=true label='Salesforce Record ID')
|
||||
public String recordId;
|
||||
|
||||
@InvocableVariable(required=false label='Language')
|
||||
public String language; // 'en' or 'es'
|
||||
|
||||
@InvocableVariable(required=false label='Email Subject')
|
||||
public String emailSubject;
|
||||
|
||||
@InvocableVariable(required=false label='Authorization to Release Form Copies')
|
||||
public Integer authReleaseFormCopies; // 1–5; only used when that template is selected
|
||||
|
||||
@InvocableVariable(required=false label='Recipient SMS Phone')
|
||||
public String recipientSmsPhone; // E.164 preferred (+15551234567); triggers SMS delivery for Docusign Recipient #1
|
||||
|
||||
@InvocableVariable(required=false label='Custom Fields')
|
||||
public Map<String, String> customFields; // For merge fields
|
||||
}
|
||||
|
||||
public class Result {
|
||||
@InvocableVariable(label='Envelope ID')
|
||||
public String envelopeId;
|
||||
|
||||
@InvocableVariable(label='Success')
|
||||
public Boolean success;
|
||||
|
||||
@InvocableVariable(label='Error Message')
|
||||
public String errorMessage;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2.2 DocusignAPIService (Service Class)
|
||||
|
||||
**Purpose**: Handles all Docusign REST API interactions
|
||||
|
||||
**Responsibilities**:
|
||||
- Construct HTTP requests
|
||||
- Make callouts to Docusign API
|
||||
- Parse responses
|
||||
- Handle errors and retries
|
||||
- Log API interactions
|
||||
|
||||
**Methods**:
|
||||
|
||||
```apex
|
||||
public static String createCompositeEnvelope(
|
||||
String envelopeJSON,
|
||||
DocusignCredentials creds
|
||||
)
|
||||
|
||||
public static HttpResponse callDocusignAPI(
|
||||
String endpoint,
|
||||
String method,
|
||||
String body,
|
||||
Map<String, String> headers
|
||||
)
|
||||
|
||||
public static String parseEnvelopeId(HttpResponse response)
|
||||
|
||||
private static void logAPICall(
|
||||
HttpRequest req,
|
||||
```
|
||||
|
|
@ -0,0 +1,185 @@
|
|||
---
|
||||
title: Migrate DocuSign templates from Sandbox to Production (Salesforce)
|
||||
---
|
||||
|
||||
# Migrate DocuSign templates from Sandbox to Production
|
||||
|
||||
Purpose
|
||||
- Step-by-step guide to: export templates from DocuSign Demo/Sandbox, import into DocuSign Production, and update (or duplicate) the corresponding Salesforce `dfsle__EnvelopeConfiguration__c` records.
|
||||
- Includes instructions to create the `Short_Name__c` custom field used by our Apex and Flows.
|
||||
|
||||
Prerequisites
|
||||
- DocuSign admin access (Demo + Production) to export/import templates.
|
||||
- Salesforce admin access to create fields and run Data Loader / sfdx commands.
|
||||
- `dfsle` (DocuSign for Salesforce) managed package installed in Production.
|
||||
- Optional: `sfdx` CLI or Data Loader for bulk changes.
|
||||
|
||||
High-level steps
|
||||
1. Export current `dfsle__EnvelopeConfiguration__c` records from Salesforce (backup).
|
||||
2. Export templates from DocuSign Demo/Sandbox and import into DocuSign Production.
|
||||
3. Record the new DocuSign template IDs (GUIDs).
|
||||
4. Create the `Short_Name__c` custom field on `dfsle__EnvelopeConfiguration__c` (if not present).
|
||||
5. Prepare CSV mapping old→new template IDs.
|
||||
6. Update existing `dfsle__EnvelopeConfiguration__c` records (or insert duplicates) with new IDs.
|
||||
7. Validate by sending test envelopes through the Flow/Apex.
|
||||
|
||||
Detailed steps
|
||||
|
||||
1) Export existing Salesforce configs (backup)
|
||||
|
||||
- Recommended fields to export:
|
||||
|
||||
```
|
||||
Id, Name, dfsle__DocuSignId__c, Short_Name__c, dfsle__EmailMessage__c
|
||||
```
|
||||
|
||||
- sfdx (CSV output):
|
||||
|
||||
```bash
|
||||
sfdx force:data:soql:query -u ORG_ALIAS -q "SELECT Id, Name, dfsle__DocuSignId__c, Short_Name__c, dfsle__EmailMessage__c FROM dfsle__EnvelopeConfiguration__c" --resultformat csv > dfsle_configs_backup.csv
|
||||
```
|
||||
|
||||
- Or use Workbench / Data Loader to export the same fields.
|
||||
|
||||
Keep this CSV safe — it is your rollback and audit record.
|
||||
|
||||
2) Export templates from DocuSign Demo/Sandbox
|
||||
|
||||
- Use DocuSign Admin UI or API to export templates. Options:
|
||||
- UI: Admin → Templates (select template) → export / download (if available).
|
||||
- API: Use eSignature REST API `GET /v2.1/accounts/{accountId}/templates/{templateId}` to retrieve template definition; then `POST /v2.1/accounts/{accountId}/templates` to create in production.
|
||||
|
||||
- After import, capture the new template GUIDs. You can find a template's ID in its template URL or via the API.
|
||||
|
||||
3) Build mapping of old → new template IDs
|
||||
|
||||
- Minimal mapping CSV for updating existing records (update by `Id`):
|
||||
|
||||
```
|
||||
Id,dfsle__DocuSignId__c
|
||||
00Nxxxxxxxxxxxx,AAAAAAAA-BBBB-CCCC-DDDD-111111111111
|
||||
00Nyyyyyyyyyyyy,22222222-3333-4444-5555-666666666666
|
||||
```
|
||||
|
||||
- Minimal CSV for inserting duplicates (keep old records intact):
|
||||
|
||||
```
|
||||
Name,Short_Name__c,dfsle__DocuSignId__c,dfsle__EmailMessage__c
|
||||
Invoice Template - EN,INV_EN,AAAAAAAA-BBBB-CCCC-DDDD-111111111111,Please sign the attached invoice
|
||||
Invoice Template - FR,INV_FR,22222222-3333-4444-5555-666666666666,Veuillez signer la facture
|
||||
```
|
||||
|
||||
4) Create the `Short_Name__c` field on `dfsle__EnvelopeConfiguration__c`
|
||||
|
||||
Why: Our Apex and Flow use `Short_Name__c` as a compact identifier. If it does not exist in Production, create it before updating/inserting records.
|
||||
|
||||
UI (recommended)
|
||||
|
||||
1. In Salesforce Setup go to **Object Manager**.
|
||||
2. Search for **Envelope Configuration** (DocuSign) or type `dfsle__EnvelopeConfiguration__c` into the quick find.
|
||||
3. Open **Fields & Relationships** → **New**.
|
||||
4. Select **Text** as the field type.
|
||||
5. Field Label: `Short Name` → Field Name (API): `Short_Name` → Length: `50` (adjust as needed) → Next.
|
||||
6. Set Field-Level Security (make visible to integration/admin profiles) → Next.
|
||||
7. Add to Page Layouts if desired → Save.
|
||||
|
||||
Notes about managed-package objects
|
||||
- `dfsle__EnvelopeConfiguration__c` is a managed-package object. You can add subscriber-org custom fields to package objects; the API name will be `Short_Name__c` (no package namespace for the custom field).
|
||||
- If you cannot create the field via the UI or need to automate it, deploy the field metadata with the Metadata API (MDAPI).
|
||||
|
||||
MDAPI example (place under `objects/dfsle__EnvelopeConfiguration__c/fields/Short_Name__c.field-meta.xml`):
|
||||
|
||||
```xml
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
|
||||
<fullName>Short_Name__c</fullName>
|
||||
<label>Short Name</label>
|
||||
<type>Text</type>
|
||||
<length>50</length>
|
||||
<inlineHelpText>Short identifier for templates used by Apex/Flows</inlineHelpText>
|
||||
</CustomField>
|
||||
```
|
||||
|
||||
Include the field in your `package.xml` and deploy with `sfdx force:mdapi:deploy` or your CI.
|
||||
|
||||
5) Apply the mapping to Salesforce
|
||||
|
||||
- Update existing records (in-place update keeps references in other automation intact):
|
||||
|
||||
```bash
|
||||
# update by Id
|
||||
sfdx force:data:bulk:upsert -s dfsle__EnvelopeConfiguration__c -f updates.csv -i Id -u ORG_ALIAS -w 10
|
||||
```
|
||||
|
||||
- Insert duplicates (if you prefer to preserve old configs):
|
||||
|
||||
```bash
|
||||
sfdx force:data:bulk:insert -s dfsle__EnvelopeConfiguration__c -f inserts.csv -u ORG_ALIAS -w 10
|
||||
```
|
||||
|
||||
- Data Loader: use the GUI to map CSV columns to fields and run Update or Insert jobs.
|
||||
|
||||
6) Validate & test
|
||||
|
||||
- Quick queries:
|
||||
|
||||
```bash
|
||||
# list configs with short names
|
||||
sfdx force:data:soql:query -u ORG_ALIAS -q "SELECT Id, Name, Short_Name__c, dfsle__DocuSignId__c FROM dfsle__EnvelopeConfiguration__c WHERE Short_Name__c != NULL" --resultformat csv
|
||||
```
|
||||
|
||||
- Send test envelopes:
|
||||
- Run the Flow that uses the templates from the UI or use the invocable Apex from Developer Console / Execute Anonymous.
|
||||
- Watch logs / debug to verify the correct `dfsle__DocuSignId__c` is used and envelopes are sent successfully.
|
||||
|
||||
Example Apex (execute as anonymous) — adapt to your codebase if method names differ:
|
||||
|
||||
```apex
|
||||
// Construct an invocable-style request object to test a single template
|
||||
// Replace types/names with your project types if they differ
|
||||
compositeEnvelope.DocusignEnvelopeRequest r = new compositeEnvelope.DocusignEnvelopeRequest();
|
||||
r.templateIds = new List<String>{'NEW-TEMPLATE-GUID'};
|
||||
r.recordId = '001xxxxxxxxxxxx';
|
||||
System.debug(JSON.serializePretty(r));
|
||||
// Call the invocable method or trigger the Flow to verify end-to-end behavior
|
||||
```
|
||||
|
||||
7) Rollback / audit
|
||||
|
||||
- Keep the original export CSV as your rollback artifact.
|
||||
- If something goes wrong, re-run Data Loader using the backup CSV to restore previous `dfsle__DocuSignId__c` values.
|
||||
|
||||
Troubleshooting & tips
|
||||
- If an insert/update fails, check field-level security and that your user has API access for the managed-package object.
|
||||
- Some fields on managed-package objects may be controlled by the package; if metadata deploy fails, create the `Short_Name__c` field via the UI.
|
||||
- If your Flow or other automation reference specific Config record Ids, prefer updating in-place rather than inserting duplicates.
|
||||
|
||||
Appendix
|
||||
|
||||
- SOQL to select configs with non-blank `Short_Name__c`:
|
||||
|
||||
```sql
|
||||
SELECT Id, Name, Short_Name__c FROM dfsle__EnvelopeConfiguration__c WHERE Short_Name__c != NULL
|
||||
```
|
||||
|
||||
- Example update CSV (updates.csv):
|
||||
|
||||
```
|
||||
Id,dfsle__DocuSignId__c
|
||||
00Nxxxxxxxxxxxx,AAAAAAAA-BBBB-CCCC-DDDD-111111111111
|
||||
00Nyyyyyyyyyyyy,22222222-3333-4444-5555-666666666666
|
||||
```
|
||||
|
||||
- Example insert CSV (inserts.csv):
|
||||
|
||||
```
|
||||
Name,Short_Name__c,dfsle__DocuSignId__c
|
||||
Invoice Template - EN,INV_EN,AAAAAAAA-BBBB-CCCC-DDDD-111111111111
|
||||
```
|
||||
|
||||
If you want, I can:
|
||||
- Export the current `dfsle__EnvelopeConfiguration__c` records from an org (provide `ORG_ALIAS`) and create the mapping CSV template for you.
|
||||
- Create the MDAPI metadata XML for `Short_Name__c` under `deploy/mdapi/objects/dfsle__EnvelopeConfiguration__c/fields/` and add it to `deploy/mdapi/package.xml` for deployment.
|
||||
|
||||
---
|
||||
Last updated: 2026-04-01
|
||||
|
|
@ -0,0 +1,170 @@
|
|||
---
|
||||
title: Migrate DocuSign templates from Sandbox to Production using DAL export (Salesforce)
|
||||
---
|
||||
|
||||
# Migrate DocuSign templates from Sandbox to Production — DAL (DocuSign Apps Launcher) export
|
||||
|
||||
Purpose
|
||||
- Use DocuSign's DAL template export/import to preserve Salesforce merge fields embedded in the DocuSign-for-Salesforce templates (`dfsle__EnvelopeConfiguration__c`).
|
||||
- This document covers exporting templates via DAL from Demo/Sandbox, importing into Production, capturing new template IDs, and updating Salesforce `dfsle__EnvelopeConfiguration__c` records.
|
||||
- Includes instructions to create the `Short_Name__c` custom field required by our Apex classes and Flow.
|
||||
|
||||
Prerequisites
|
||||
- DocuSign admin access (Demo/Sandbox + Production) with permission to export/import templates via DAL.
|
||||
- Salesforce admin access to create fields and run Data Loader / `sfdx` commands.
|
||||
- `dfsle` (DocuSign for Salesforce) managed package installed in Production.
|
||||
- Optional: `sfdx` CLI or Data Loader for bulk CSV updates.
|
||||
|
||||
High-level steps
|
||||
1. Export the DocuSign templates from Sandbox/Demo using DAL export.
|
||||
2. Import the DAL package(s) into DocuSign Production.
|
||||
3. Capture the new DocuSign template IDs for each imported template.
|
||||
4. Create the `Short_Name__c` custom field on `dfsle__EnvelopeConfiguration__c` (if missing).
|
||||
5. Prepare CSV mapping old→new template IDs.
|
||||
6. Update (or insert duplicates of) `dfsle__EnvelopeConfiguration__c` records in Salesforce.
|
||||
7. Validate by sending test envelopes through the Flow/Apex.
|
||||
|
||||
Detailed steps
|
||||
|
||||
1) Download templates from the source Salesforce org (DAL)
|
||||
|
||||
- Preconditions: you must have Salesforce and DocuSign admin permissions and DAL (DocuSign Apps Launcher v4.5+) installed in the org.
|
||||
|
||||
- Steps in the source org:
|
||||
1. Open the Salesforce App Launcher (grid icon) and select View All.
|
||||
2. Select DocuSign Apps Launcher.
|
||||
3. In the left column select eSignature to open the eSignature Configuration page.
|
||||
4. On the eSignature Configuration page, locate the list of Envelope Templates.
|
||||
5. Select the checkbox next to each template you want to download.
|
||||
6. From the Create Template dropdown choose Download Template.
|
||||
7. The page will refresh and show "Templates downloaded". Save the downloaded file(s) (ZIP) to your device.
|
||||
- Note: Salesforce prevents downloading templates larger than 4 MB.
|
||||
|
||||
- Important: templates that include custom fields created by a specific user may not import cleanly into the destination org unless that creating user exists in the destination. Either add the creating user to the destination org before upload, or plan to rebuild the missing custom fields after import.
|
||||
|
||||
2) Import DAL package(s) into DocuSign Production
|
||||
|
||||
- In DocuSign Production, go to the Templates (or Template Management) area and use the **Import** or **Upload DAL package** option.
|
||||
- Upload each DAL package file exported from Sandbox/Demo.
|
||||
- After import, open each imported template and verify that merge fields (placeholders) are preserved and that the template content looks correct.
|
||||
|
||||
3) Capture new template IDs (GUIDs)
|
||||
|
||||
- For each imported template record in Production, record the new template ID (GUID). The template ID is often visible in the template URL (e.g., `/templates/{templateId}`) or in the template properties.
|
||||
- Create a simple mapping list with columns: `OldDocuSignId, NewDocuSignId, TemplateName`.
|
||||
|
||||
4) Create `Short_Name__c` on `dfsle__EnvelopeConfiguration__c` (if not present)
|
||||
|
||||
Why: Our Apex and Flow expect a `Short_Name__c` field as a compact identifier. Create it before inserting/updating records so `Short_Name__c` values can be included in CSVs and UI pages.
|
||||
|
||||
UI steps (recommended):
|
||||
|
||||
1. In Salesforce Setup, open **Object Manager**.
|
||||
2. Search for **Envelope Configuration** (DocuSign) or look up `dfsle__EnvelopeConfiguration__c`.
|
||||
3. Click **Fields & Relationships** → **New**.
|
||||
4. Select **Text** → **Next**.
|
||||
5. Label: `Short Name` → API Name will be `Short_Name__c` → Length: `50` → Next.
|
||||
6. Set Field-Level Security (visible to integration/admin profiles) → Next → Save.
|
||||
|
||||
Managed-package note
|
||||
- Adding custom fields to managed-package objects is supported in subscriber orgs; the custom field's API name will be `Short_Name__c` (no package namespace).
|
||||
|
||||
MDAPI (optional automation)
|
||||
- If you prefer to deploy the field via Metadata API, create a field metadata file:
|
||||
|
||||
```xml
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
|
||||
<fullName>Short_Name__c</fullName>
|
||||
<label>Short Name</label>
|
||||
<type>Text</type>
|
||||
<length>50</length>
|
||||
<inlineHelpText>Short identifier used by Apex and Flow</inlineHelpText>
|
||||
</CustomField>
|
||||
```
|
||||
|
||||
Place that at `deploy/mdapi/objects/dfsle__EnvelopeConfiguration__c/fields/Short_Name__c.field-meta.xml` and add the file to `deploy/mdapi/package.xml` before an `sfdx force:mdapi:deploy`.
|
||||
|
||||
5) Prepare mapping CSV (old → new)
|
||||
|
||||
- For updating existing Salesforce records (recommended if Flows reference record Ids):
|
||||
|
||||
```
|
||||
Id,dfsle__DocuSignId__c
|
||||
00Nxxxxxxxxxxxx,AAAAAAAA-BBBB-CCCC-DDDD-111111111111
|
||||
00Nyyyyyyyyyyyy,22222222-3333-4444-5555-666666666666
|
||||
```
|
||||
|
||||
- For inserting duplicates (preserve old configs):
|
||||
|
||||
```
|
||||
Name,Short_Name__c,dfsle__DocuSignId__c,dfsle__EmailMessage__c
|
||||
Invoice Template - EN,INV_EN,AAAAAAAA-BBBB-CCCC-DDDD-111111111111,Please sign the attached invoice
|
||||
```
|
||||
|
||||
6) Update or insert `dfsle__EnvelopeConfiguration__c` records in Salesforce
|
||||
|
||||
- Update (in-place):
|
||||
|
||||
```bash
|
||||
# update by Id (bulk upsert)
|
||||
sfdx force:data:bulk:upsert -s dfsle__EnvelopeConfiguration__c -f updates.csv -i Id -u ORG_ALIAS -w 10
|
||||
```
|
||||
|
||||
- Insert (duplicates):
|
||||
|
||||
```bash
|
||||
sfdx force:data:bulk:insert -s dfsle__EnvelopeConfiguration__c -f inserts.csv -u ORG_ALIAS -w 10
|
||||
```
|
||||
|
||||
- Alternatively use Data Loader / Dataloader.io for GUI-driven imports.
|
||||
|
||||
Notes
|
||||
- If flows or other automation reference the `dfsle__EnvelopeConfiguration__c` record Ids directly, prefer updating `dfsle__DocuSignId__c` in-place.
|
||||
- If you want to keep previous configs for rollback or comparison, insert duplicates and then re-point any automation to new records as needed.
|
||||
|
||||
7) Validate end-to-end
|
||||
|
||||
- Query to verify `Short_Name__c` and new DocuSign IDs:
|
||||
|
||||
```bash
|
||||
sfdx force:data:soql:query -u ORG_ALIAS -q "SELECT Id, Name, Short_Name__c, dfsle__DocuSignId__c FROM dfsle__EnvelopeConfiguration__c WHERE Short_Name__c != NULL" --resultformat csv
|
||||
```
|
||||
|
||||
- Trigger the Flow or call the Invocable Apex (in Developer Console or by running the Flow in UI) to send a test envelope and confirm the imported template is used and merge fields populate as expected.
|
||||
|
||||
Rollback
|
||||
- Keep your original export CSV from step 1 as a rollback file. To revert, re-run Data Loader using the backup CSV to restore prior `dfsle__DocuSignId__c` values.
|
||||
|
||||
Troubleshooting
|
||||
- If a bulk insert/update fails, check Field-Level Security, API access for the managed-package object, and required fields enforced by the package.
|
||||
- If the DAL import does not preserve some merge mappings, capture the problem template and contact DocuSign support — but DAL is the recommended method because it preserves Salesforce-related merge fields.
|
||||
|
||||
Appendix
|
||||
|
||||
- Example SOQL to list templates with non-blank `Short_Name__c`:
|
||||
|
||||
```sql
|
||||
SELECT Id, Name, Short_Name__c FROM dfsle__EnvelopeConfiguration__c WHERE Short_Name__c != NULL
|
||||
```
|
||||
|
||||
- Example `updates.csv`:
|
||||
|
||||
```
|
||||
Id,dfsle__DocuSignId__c
|
||||
00Nxxxxxxxxxxxx,AAAAAAAA-BBBB-CCCC-DDDD-111111111111
|
||||
```
|
||||
|
||||
- Example `inserts.csv`:
|
||||
|
||||
```
|
||||
Name,Short_Name__c,dfsle__DocuSignId__c
|
||||
Invoice Template - EN,INV_EN,AAAAAAAA-BBBB-CCCC-DDDD-111111111111
|
||||
```
|
||||
|
||||
If you want, I can:
|
||||
- Export the current `dfsle__EnvelopeConfiguration__c` records from an org (provide `ORG_ALIAS`) and prepare `updates.csv`/`inserts.csv` pre-filled with current data.
|
||||
- Add the MDAPI field metadata for `Short_Name__c` into `deploy/mdapi/objects/dfsle__EnvelopeConfiguration__c/fields/` and update `deploy/mdapi/package.xml` for deployment.
|
||||
|
||||
---
|
||||
Last updated: 2026-04-01
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
# Migrate DocuSign templates from Sandbox to Production — DAL (DocuSign Apps Launcher) export
|
||||
|
||||
# Purpose
|
||||
- Use DocuSign's DAL template export/import to preserve Salesforce merge fields embedded in the DocuSign-for-Salesforce templates (`dfsle__EnvelopeConfiguration__c`).
|
||||
- This document covers exporting templates via DAL from Demo/Sandbox, importing into Production, capturing new template IDs, and updating Salesforce `dfsle__EnvelopeConfiguration__c` records.
|
||||
- Includes instructions to create the `Short_Name__c` custom field required by our Apex classes and Flow.
|
||||
|
||||
# Prerequisites
|
||||
- DocuSign admin access (Demo/Sandbox + Production) with permission to export/import templates via DAL.
|
||||
- Salesforce admin access to create fields and run Data Loader / `sfdx` commands.
|
||||
- `dfsle` (DocuSign for Salesforce) managed package installed in Production.
|
||||
- Optional: `sfdx` CLI or Data Loader for bulk CSV updates.
|
||||
|
||||
# High-level steps
|
||||
1. Export the DocuSign templates from Sandbox/Demo using DAL export.
|
||||
2. Import the DAL package(s) into DocuSign Production.
|
||||
3. Capture the new DocuSign template IDs for each imported template.
|
||||
4. Create the `Short_Name__c` custom field on `dfsle__EnvelopeConfiguration__c` (if missing).
|
||||
5. Prepare CSV mapping old→new template IDs.
|
||||
6. Update (or insert duplicates of) `dfsle__EnvelopeConfiguration__c` records in Salesforce.
|
||||
7. Validate by sending test envelopes through the Flow/Apex.
|
||||
|
||||
# Detailed steps
|
||||
|
||||
## 1) Download templates from the source Salesforce org
|
||||
|
||||
- Preconditions: you must have Salesforce and DocuSign admin permissions and DAL (DocuSign Apps Launcher v4.5+) installed in the org.
|
||||
|
||||
### Steps in the source org:
|
||||
1. Open the Salesforce App Launcher (grid icon) and select View All.
|
||||
2. Select DocuSign Apps Launcher.
|
||||
3. In the left column select eSignature to open the eSignature Configuration page.
|
||||
4. The Templates tab should be showing
|
||||
5. On the eSignature Configuration page, locate the list of Envelope Templates.
|
||||
6. Select the checkbox next to each template you want to download.
|
||||
7. From the Create Template dropdown choose Download Template.
|
||||
8. The page will refresh and show "Templates downloaded". Save the downloaded file(s) (ZIP) to your device.
|
||||
- Note: Salesforce prevents downloading templates larger than 4 MB.
|
||||
|
||||
- Important: templates that include custom fields created by a specific user may not import cleanly into the destination org unless that creating user exists in the destination. Either add the creating user to the destination org before upload, or plan to rebuild the missing custom fields after import.
|
||||
|
||||
## 2) Upload templates to the target Salesforce org
|
||||
|
||||
### Steps in the target org:
|
||||
1. Open the Salesforce App Launcher (grid icon) and select View All.
|
||||
2. Select DocuSign Apps Launcher.
|
||||
3. In the left column select eSignature to open the eSignature Configuration page.
|
||||
4. The Templates tab should be showing
|
||||
5. From the Create Template dropdown choose Upload Template.
|
||||
|
||||
- Upload each Template file exported from the source org.
|
||||
- After upload, open each template and verify that merge fields (placeholders) are preserved and that the template content looks correct.
|
||||
|
||||
## 3) Create `Short_Name__c` on `dfsle__EnvelopeConfiguration__c` (if not present)
|
||||
|
||||
Why: Our Apex and Flow expect a `Short_Name__c` field as a compact identifier. Create it before inserting/updating records so `Short_Name__c` values can be included in CSVs and UI pages.
|
||||
|
||||
### Steps for adding the Short_Name field:
|
||||
1. Go to the Docusign Apps Launch eSignature Configuration page (should already be there from last step)
|
||||
2. Click on Name of the top template (you can select any template). This will bring you into the template
|
||||
3. Go to top Gear Icon Settings menu and Select "Edit Object"
|
||||
4. This brings you to the Setup Object Manager for the Docusign Envelope Template
|
||||
5. Click **Fields & Relationships** in left hand menu, then → **New**.
|
||||
6. Select **Text** → **Next**.
|
||||
7. Label: `Short Name` → API Name will be `Short_Name__c` → Length: `50` → Next.
|
||||
8. Set Field-Level Security (visible to all profiles) → Next → Save.
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
%PDF-1.3
|
||||
%“Œ‹ž ReportLab Generated PDF document (opensource)
|
||||
1 0 obj
|
||||
<<
|
||||
/F1 2 0 R /F2 3 0 R /F3 4 0 R
|
||||
>>
|
||||
endobj
|
||||
2 0 obj
|
||||
<<
|
||||
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
|
||||
>>
|
||||
endobj
|
||||
3 0 obj
|
||||
<<
|
||||
/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font
|
||||
>>
|
||||
endobj
|
||||
4 0 obj
|
||||
<<
|
||||
/BaseFont /Symbol /Name /F3 /Subtype /Type1 /Type /Font
|
||||
>>
|
||||
endobj
|
||||
5 0 obj
|
||||
<<
|
||||
/Contents 10 0 R /MediaBox [ 0 0 612 792 ] /Parent 9 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
6 0 obj
|
||||
<<
|
||||
/Contents 11 0 R /MediaBox [ 0 0 612 792 ] /Parent 9 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
7 0 obj
|
||||
<<
|
||||
/PageMode /UseNone /Pages 9 0 R /Type /Catalog
|
||||
>>
|
||||
endobj
|
||||
8 0 obj
|
||||
<<
|
||||
/Author (anonymous) /CreationDate (D:20260402234617-04'00') /Creator (anonymous) /Keywords () /ModDate (D:20260402234617-04'00') /Producer (ReportLab PDF Library - \(opensource\))
|
||||
/Subject (unspecified) /Title (untitled) /Trapped /False
|
||||
>>
|
||||
endobj
|
||||
9 0 obj
|
||||
<<
|
||||
/Count 2 /Kids [ 5 0 R 6 0 R ] /Type /Pages
|
||||
>>
|
||||
endobj
|
||||
10 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1476
|
||||
>>
|
||||
stream
|
||||
Gatm;gJ6d"&:L1SW4*3ti5CX/KK#3I(5^ibM%!1BTOV'ihQbnnDB"tXhs.Wj4!@J\'g`7iC$Ka(e2_@b-M1f;cS,Ir@fbUO)f,`pCTq*fKuF/L0#!_9%bR-E\RSimpY9XQGV0-E";Vl6ro11n-,No?eK^,r-o!5MHD4e!70\f(O&k6\8oY&nIphgDa&)adYWNTc]+e*WQ<`LTFIW?nf+5)*D*hSGKn9P1j^Ea]Gm=%M#[)&c2(_Nr9?s2bI(-)KP(c_W>1@srq_;s&l"k&=_0uB3^N]%"lB(p<RJpDQRr68/e%Z/a-mU:,Y=KM.W7F:i#T:c%@EgNd'O\Ad.)H77IQ7)?24;X`bBuU\/p]gqOY0-Wd9<CAr-Y!K@O__t&c%m9'lec$pA>oE/C1CrYj8"Dc0SA*lAdG/XrsHJ1X&Jt`@FR2^r,cR=7.SO#"-;H3G)aUd.&)j)"hm?+6'.m)U*AlGn(uJb"CrKc1.p*ci%+>",2N1LmX%S6$PB'hi@[aYTPY71/`b`p"U4rD%>Cb@9Q)&*R@_7l].5W>:<?-WG1qoQk4AcJ$TPX?"'R%_Ujm0MYssCOuTrI"eq0eZhSu/X4rXk9USeC<o\CjMRO*j*o11AdV9&V)AFP'I**5:_+@kd*SUi8M_\&"@>IPE:7m+,'fo*PQG)fDJHBE&Q.#M!ODc`KiO"'?^q6ZiaqNAgBaq[a4b_O'Zsub(6[G]:b$iRf"qQ;S-t*N;C)su$HUO1IJ\e^:Vm@%oD%>+>p!Pc4._N6`1]aV.`C/L5Kg4g[f`7>87Qu1*)I01])LFB_^(i[<9<VNPDBA4sMX@1:MdKKK:Uk[tWq?Q\!dJfuVR,!os5\0?OWs$"W"P$7fp\nto#.upp:WAdjQK$@XHs&>V=db[GJ+,H6(ba]g`6djZS",SCSn]`.2oa_riVpo5ND"8*T#:t)e0Dd<,.'\d'.[#1I5P[dMNS$XSqbKjA%9,/u:a/Ue+pRgWacWf*Cb\;nJU'&Ze)9DYq_\SrmXtnlRSt9X?`h7A5Jid`;P2hbCT>N0]Le2h0N+0\Wqp_%g7aA+VAJ@CD%iTL"\S+E8g0MNimNZ=6Q#3SC\K?GL[2^thl:_.@OC/[4/iV;)b[ri7`6l3i_(!Lk9)T+`ZR3&*8;?9?^Z4^:,iVf=dL):pW(5W^C(CS(CRjkeqhV:2FL!Bh6;GtQ5gWu8JN7CS4plOGIVr$_tbHdh#dh&cEq7nmrl$e#jg&f"raEfdNm$bH&KSbk_9qR'=@^\&SllqSIT[bcr#e[<&n7bi=hV3)s,p.]hYqd1EJgTTk@LYG:50W0G&T7Z^:mK8!@i`gtVOre;d@+X:V'MD1qL$%6f!E5@p=Mb`LrP)+VU&20i)?BTP@i]Cc9BX[K_5%B2^2RGgQ/Yu^Z_UTe[tUSP`+ma4+BebkO11@-4=hO)&mheVP)Nj9i>7th.bP/.+t@Vf($]^UU`-rAnDum4Q[.3~>endstream
|
||||
endobj
|
||||
11 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1726
|
||||
>>
|
||||
stream
|
||||
Gatm;mn_\!'`Gb\iee7BV;cWn:"mHC`aD,B0Km!1l)><*a\D*G[3OEToY9:kj.u'E#=2.L-BU>QkPdO*-A#b((]"*[-,>9B]YBCJlGFm&..5&.\V?$a]DD?.J)9'FNPYij%&:WU-;3P&,X&Z&?l')na6/W=;Bq\^RIb^'RH7%-_<,tF6D_o#5:O+ITQ7Q(.ho!KR,G7]oW.3s_W3dP]!(8D:O5T?1@2gt$;@SB8/IZmR&MHppd@ml+A3MY!t_P6$'.dV?S_:&Kjn2#n7WB+7m4B3C?^;q2*-7K,fm>B*!.imarl'?B[)CbYfFdYbi9a<#h*?%*k;iT1"U(S^gmX]TcW0ca__\=Ad=PM[]54iGmZPJ3:3*ti@^lWm,>ISqm>tPeX,Nob8t4m6\[+_[Z4lXWiJ(<7\ejn%Yn9/DNr+=U+XgR8uEB5`YD+XM2f](;e,u^krEJcP=^Sho7tm"PL1oEKgSqb6>9(UFh5PBTIk,NMDCru/<-JYaZ57&5jB:7K@q.[Qi^3Xo+XeaKU6Ka;ULF%b5KD$*"nGBA5]K8M(7McMT0[>c(EF*-mdt+d.+n(;'$+;BGq>nq<NC@rO@Ko8%mjQM?]AXe]@.5]8+H9WH&8UiMq#gO+8u3h._kTOpcciIcT]<7(hU#33:r+ZJ6(7Q*t>EmZYfh@LmhGhj->aW<Tq0Sc.(Pgob0&O**$c[sO42H;.Uhr822T2L[CgdG7qVR[O@M_E!#bcjO-M%#0al6mUXn<]F;<SJ;<5ZVm['^=1A4o*flD,ULG\9;SuP3'P3I&8fH;l,Nh=he;^RQ'T]\*40Fa>MgDh9k]/#>ZH=]bc0on.$q6:^1C>E=B29WNull<9RLjnW5OrpDku$kR?Qfsg3n]^W2Z-<?9d;20kk&]TsNWk5Oi^jZ6n.X!g`T;L6/_ZOrZKVc?*gXL5_oHVq.)q=8Br"fU8(_7Y=t=GLlrKj=A_B?WWE$6lNmFA>UA.Pi>#d%(1("TtM,%lE)bp%j+m<ipQN,qUr"`Bt,]S)6HKq#C1sMRbG-^KZ-gE'E]Np>2eFX6+O_E@8"8J:EP,`q'!q@TYKb,Co?%DTtMtl+E[:`_fK`o5&t@eh&>,u:4luWnGb,lVf5;P`E@af^*8nq()gp-2o+c_<5(E/a93o>bC\+bSe3]_<>(HU(OV:73p9u4_4nH8JiZ,C?PZdUCfD(EW<8**L;%,kUqogJb@Xj<Ic_pRr9g'EoY4`"LMiX;$-i'!pdOKcoT-J?KD7N0p</O2p4d=IV#SJ)qOnMB69_C&*hA7Z?Y')\W_2:cP`Q2$;7:ZDF*klt/\iMgb3-`20`%:n'_putE;QA,AOL&rkCTTrggp1:dDq*9:/u*WFP:^V%BdL_6^V+;^J5;d5T7sC[>*#7b<;ER+2b=+Wb,nEmqNe0>h*90e:C_O6Di_!7"tQh]&#<?@C#NR(``;.,3'GVBA_s7K=7/l$K8+H_Y+Q\s'6AV+lKX3pVUVjR9RX9BC>E6ZZ!Xf9YrEEe0XI)0.WIi$iWHNqogU9@Cf`'N.)LD\1G.3*'#Va4QQC__%'@Ug/!I^QX((A_RRG&DRZ`Dopb>Zo<$a6:?VeFaM!F`Ha>;^.*O8kZsL'VMUS50^@NH#o/QVf<=M[Qq9SH*oo7QOkB9/(P2eL6mBY/$-]OjZ?"GS^*r>c&^+KTfSX,`!2(?=u)a4paB4M]mTp8hog^`g#RdK!QTC(CL_7Q=A_!hI~>endstream
|
||||
endobj
|
||||
xref
|
||||
0 12
|
||||
0000000000 65535 f
|
||||
0000000061 00000 n
|
||||
0000000112 00000 n
|
||||
0000000219 00000 n
|
||||
0000000331 00000 n
|
||||
0000000408 00000 n
|
||||
0000000602 00000 n
|
||||
0000000796 00000 n
|
||||
0000000864 00000 n
|
||||
0000001125 00000 n
|
||||
0000001190 00000 n
|
||||
0000002758 00000 n
|
||||
trailer
|
||||
<<
|
||||
/ID
|
||||
[<74d07b662bedb5b80492225fc042af8b><74d07b662bedb5b80492225fc042af8b>]
|
||||
% ReportLab generated PDF document -- digest (opensource)
|
||||
|
||||
/Info 8 0 R
|
||||
/Root 7 0 R
|
||||
/Size 12
|
||||
>>
|
||||
startxref
|
||||
4576
|
||||
%%EOF
|
||||
|
|
@ -0,0 +1,192 @@
|
|||
#!/usr/bin/env python3
|
||||
import sys
|
||||
from pathlib import Path
|
||||
import textwrap
|
||||
import re
|
||||
|
||||
try:
|
||||
from reportlab.lib.pagesizes import letter
|
||||
from reportlab.pdfgen import canvas
|
||||
from reportlab.lib.units import inch
|
||||
from reportlab.lib import colors
|
||||
except Exception as e:
|
||||
print('Missing reportlab:', e)
|
||||
sys.exit(2)
|
||||
|
||||
|
||||
def wrap_text(text, max_chars):
|
||||
return textwrap.wrap(text, width=max_chars)
|
||||
|
||||
|
||||
def draw_wrapped(c, text, x, y, font_name, font_size, max_width, indent=0):
|
||||
# Estimate chars per line roughly
|
||||
approx_char_width = font_size * 0.55
|
||||
max_chars = max(20, int((max_width - indent) / approx_char_width))
|
||||
wrapped = wrap_text(text, max_chars)
|
||||
line_height = font_size * 1.25
|
||||
for line in wrapped:
|
||||
c.drawString(x + indent, y, line)
|
||||
y -= line_height
|
||||
return y
|
||||
#!/usr/bin/env python3
|
||||
import sys
|
||||
from pathlib import Path
|
||||
import textwrap
|
||||
|
||||
try:
|
||||
from reportlab.lib.pagesizes import letter
|
||||
from reportlab.pdfgen import canvas
|
||||
from reportlab.lib.units import inch
|
||||
from reportlab.lib import colors
|
||||
except Exception as e:
|
||||
print('Missing reportlab:', e)
|
||||
sys.exit(2)
|
||||
|
||||
|
||||
def wrap_text(text, max_chars):
|
||||
return textwrap.wrap(text, width=max_chars)
|
||||
|
||||
|
||||
def draw_wrapped(c, text, x, y, font_name, font_size, max_width, indent=0):
|
||||
# Estimate chars per line roughly
|
||||
approx_char_width = font_size * 0.55
|
||||
max_chars = max(20, int((max_width - indent) / approx_char_width))
|
||||
wrapped = wrap_text(text, max_chars)
|
||||
line_height = font_size * 1.25
|
||||
for line in wrapped:
|
||||
c.drawString(x + indent, y, line)
|
||||
y -= line_height
|
||||
return y
|
||||
|
||||
|
||||
if len(sys.argv) < 3:
|
||||
print('Usage: md_to_pdf.py input.md output.pdf')
|
||||
sys.exit(1)
|
||||
|
||||
in_path = Path(sys.argv[1])
|
||||
out_path = Path(sys.argv[2])
|
||||
|
||||
if not in_path.exists():
|
||||
print('Input file not found:', in_path)
|
||||
sys.exit(1)
|
||||
|
||||
text = in_path.read_text(encoding='utf-8')
|
||||
lines = text.splitlines()
|
||||
|
||||
c = canvas.Canvas(str(out_path), pagesize=letter)
|
||||
width, height = letter
|
||||
left = inch * 0.75
|
||||
right = inch * 0.75
|
||||
top = height - inch * 0.75
|
||||
bottom = inch * 0.75
|
||||
max_width = width - left - right
|
||||
|
||||
# default fonts
|
||||
REGULAR = 'Helvetica'
|
||||
BOLD = 'Helvetica-Bold'
|
||||
MONO = 'Courier'
|
||||
|
||||
y = top
|
||||
|
||||
in_code = False
|
||||
code_font_size = 8
|
||||
para_font_size = 10
|
||||
|
||||
for raw in lines:
|
||||
line = raw.rstrip('\n')
|
||||
if line.strip() == '':
|
||||
y -= para_font_size * 0.6
|
||||
if y < bottom:
|
||||
c.showPage()
|
||||
y = top
|
||||
continue
|
||||
|
||||
# Code fence toggle
|
||||
if line.strip().startswith('```'):
|
||||
in_code = not in_code
|
||||
if in_code:
|
||||
y -= 6
|
||||
else:
|
||||
y -= 6
|
||||
if y < bottom:
|
||||
c.showPage()
|
||||
y = top
|
||||
continue
|
||||
|
||||
if in_code:
|
||||
c.setFont(MONO, code_font_size)
|
||||
# draw code line with small left indent
|
||||
y = draw_wrapped(c, line, left, y, MONO, code_font_size, max_width, indent=10)
|
||||
c.setFont(REGULAR, para_font_size)
|
||||
if y < bottom:
|
||||
c.showPage()
|
||||
y = top
|
||||
continue
|
||||
|
||||
# Headings
|
||||
if line.lstrip().startswith('#'):
|
||||
hashes, _, rest = line.partition(' ')
|
||||
level = hashes.count('#')
|
||||
text = rest.strip()
|
||||
# add extra space BEFORE top-level headings so they don't butt against previous paragraph
|
||||
if level == 1:
|
||||
y -= para_font_size * 1.8
|
||||
elif level == 2:
|
||||
y -= para_font_size * 0.8
|
||||
else:
|
||||
y -= para_font_size * 0.6
|
||||
|
||||
if level == 1:
|
||||
font_size = 18
|
||||
font = BOLD
|
||||
elif level == 2:
|
||||
font_size = 14
|
||||
font = BOLD
|
||||
else:
|
||||
font_size = 12
|
||||
font = BOLD
|
||||
c.setFont(font, font_size)
|
||||
y = draw_wrapped(c, text, left, y, font, font_size, max_width)
|
||||
# modest spacing after headings
|
||||
y -= para_font_size * 0.6
|
||||
c.setFont(REGULAR, para_font_size)
|
||||
if y < bottom:
|
||||
c.showPage()
|
||||
y = top
|
||||
continue
|
||||
|
||||
# Horizontal rule
|
||||
if line.strip() in ('---', '***', '___'):
|
||||
c.setStrokeColor(colors.black)
|
||||
c.setLineWidth(1)
|
||||
y -= 6
|
||||
c.line(left, y, left + max_width, y)
|
||||
y -= 12
|
||||
if y < bottom:
|
||||
c.showPage()
|
||||
y = top
|
||||
continue
|
||||
|
||||
# Lists
|
||||
stripped = line.lstrip()
|
||||
if stripped.startswith(('- ', '* ', '+ ')) or stripped[:2].isdigit() and stripped[2:].startswith('. '):
|
||||
bullet = '•'
|
||||
content = stripped[2:].strip() if not stripped[0].isdigit() else stripped.split('.', 1)[1].strip()
|
||||
c.setFont(REGULAR, para_font_size)
|
||||
# draw bullet and wrapped content with indent
|
||||
c.drawString(left, y, bullet)
|
||||
y = draw_wrapped(c, content, left + 12, y, REGULAR, para_font_size, max_width, indent=0)
|
||||
if y < bottom:
|
||||
c.showPage()
|
||||
y = top
|
||||
continue
|
||||
|
||||
# Paragraph
|
||||
c.setFont(REGULAR, para_font_size)
|
||||
y = draw_wrapped(c, line, left, y, REGULAR, para_font_size, max_width)
|
||||
if y < bottom:
|
||||
c.showPage()
|
||||
y = top
|
||||
|
||||
c.save()
|
||||
print('Wrote PDF:', out_path)
|
||||
Loading…
Reference in New Issue