refactor: extract request handling to global DocusignEnvelopeRequestHandler class + deployment guide
Changes: - Move validation logic to DocusignEnvelopeRequestHandler.validateRequest() - Move envelope JSON building to DocusignEnvelopeRequestHandler.buildEnvelopeJSON() - Add comprehensive test suite for handler (10 test methods) - Simplify main invocable method - focus on orchestration - Add DEPLOYMENT_AND_TESTING.md reference guide with all CLI commands - Handler is reusable for future enhancements and integrations Benefits: - Single Responsibility Principle - Reusable handler for other classes - Better testability - Clean separation of concerns
This commit is contained in:
parent
4f734f0d17
commit
2f7dcf3520
|
|
@ -0,0 +1,218 @@
|
||||||
|
# Deployment & Testing Guide
|
||||||
|
|
||||||
|
Quick reference for deploying the Salesforce Composite Envelope Builder to your org and running tests.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick Start (Recommended)
|
||||||
|
|
||||||
|
### Deploy + Test in One Script
|
||||||
|
```bash
|
||||||
|
cd /home/paulh/.openclaw/workspace/projects/salesforce-composite-envelope-builder/composite-envelope-builder
|
||||||
|
bash deploy-to-dev-org.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
This script handles:
|
||||||
|
- ✅ Org authorization (opens browser login)
|
||||||
|
- ✅ Code deployment
|
||||||
|
- ✅ Unit test execution with code coverage
|
||||||
|
- ✅ Human-readable results
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Manual Commands
|
||||||
|
|
||||||
|
### 1. Authorize Your Org (First Time Only)
|
||||||
|
```bash
|
||||||
|
# For Developer Edition
|
||||||
|
sf org login web --alias dev-org --instance-url https://login.salesforce.com
|
||||||
|
|
||||||
|
# For Sandbox
|
||||||
|
sf org login web --alias sandbox-org --instance-url https://test.salesforce.com
|
||||||
|
```
|
||||||
|
|
||||||
|
You'll be redirected to Salesforce login. Once authorized, the org alias is saved for future deploys.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Deploy Code
|
||||||
|
|
||||||
|
**Deploy all code to your org:**
|
||||||
|
```bash
|
||||||
|
cd /home/paulh/.openclaw/workspace/projects/salesforce-composite-envelope-builder/composite-envelope-builder
|
||||||
|
sf project deploy start --target-org dev-org
|
||||||
|
```
|
||||||
|
|
||||||
|
**Options:**
|
||||||
|
- `--target-org dev-org` — Use the authorized org alias (replace with your alias if different)
|
||||||
|
- `--wait 10` — Wait up to 10 minutes for deployment to complete
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. Run Tests
|
||||||
|
|
||||||
|
**Run ALL unit tests with code coverage:**
|
||||||
|
```bash
|
||||||
|
sf apex run test --wait 10 --result-format human --code-coverage --target-org dev-org
|
||||||
|
```
|
||||||
|
|
||||||
|
**Run ONLY the new handler tests:**
|
||||||
|
```bash
|
||||||
|
sf apex run test --class-names DocusignEnvelopeRequestHandlerTest --wait 10 --result-format human --target-org dev-org
|
||||||
|
```
|
||||||
|
|
||||||
|
**Run specific test methods:**
|
||||||
|
```bash
|
||||||
|
sf apex run test --class-names DocusignEnvelopeRequestHandlerTest --method-names testValidateRequest_Success --wait 10 --result-format human --target-org dev-org
|
||||||
|
```
|
||||||
|
|
||||||
|
**Run ALL tests with JSON output (for parsing):**
|
||||||
|
```bash
|
||||||
|
sf apex run test --wait 10 --result-format json --code-coverage --target-org dev-org > test-results.json
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Complete Deploy + Test in One Command
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /home/paulh/.openclaw/workspace/projects/salesforce-composite-envelope-builder/composite-envelope-builder && \
|
||||||
|
sf project deploy start --target-org dev-org && \
|
||||||
|
sf apex run test --wait 10 --result-format human --code-coverage --target-org dev-org
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Understanding Test Results
|
||||||
|
|
||||||
|
Sample output:
|
||||||
|
```
|
||||||
|
Apex Tests
|
||||||
|
═════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
Test Results Summary [Included Code Coverage]
|
||||||
|
═════════════════════════════════════════════════════════════════
|
||||||
|
Outcome: Passed
|
||||||
|
Tests Ran: 39
|
||||||
|
Passes: 39
|
||||||
|
Failures: 0
|
||||||
|
Skipped: 0
|
||||||
|
Pass Rate: 100%
|
||||||
|
Apex Code Coverage: 92%
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key metrics:**
|
||||||
|
- **Pass Rate** — Percentage of tests that passed (should be 100%)
|
||||||
|
- **Apex Code Coverage** — Percentage of code executed by tests (should be >80%)
|
||||||
|
- **Failures** — If > 0, review the failed test names and error messages below
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Post-Deployment Setup
|
||||||
|
|
||||||
|
After successful deployment, configure your Salesforce org:
|
||||||
|
|
||||||
|
### 1. Create Docusign Configuration Custom Setting
|
||||||
|
|
||||||
|
1. Log into your Salesforce org
|
||||||
|
2. Go to **Setup** → **Custom Settings**
|
||||||
|
3. Click **Manage** next to **Docusign Configuration**
|
||||||
|
4. Click **New**
|
||||||
|
5. Fill in:
|
||||||
|
- **Account Id:** `{your Docusign sandbox account ID}`
|
||||||
|
- **Base URL:** `callout:DocusignAPI`
|
||||||
|
6. Click **Save**
|
||||||
|
|
||||||
|
### 2. Create Named Credential for Docusign API
|
||||||
|
|
||||||
|
1. Go to **Setup** → **Named Credentials**
|
||||||
|
2. Click **New Named Credential**
|
||||||
|
3. Fill in:
|
||||||
|
- **Name:** `DocusignAPI`
|
||||||
|
- **Label:** `Docusign API`
|
||||||
|
- **URL:** `https://demo.docusign.net/restapi/v2.1`
|
||||||
|
- **Identity Type:** Named Principal
|
||||||
|
- **Authentication Protocol:** OAuth 2.0
|
||||||
|
- **Authentication Provider:** DocusignOAuthProvider (if available)
|
||||||
|
4. Click **Save**
|
||||||
|
|
||||||
|
### 3. Update Your Screen Flow
|
||||||
|
|
||||||
|
1. Go to **Flow Builder**
|
||||||
|
2. Create or edit your Screen Flow
|
||||||
|
3. Add an **Action** to the flow:
|
||||||
|
- Action: **Send Composite Docusign Envelope**
|
||||||
|
- Input Variables:
|
||||||
|
- **Template IDs** — Comma-separated Docusign template IDs
|
||||||
|
- **Salesforce Record ID** — {!Record.Id}
|
||||||
|
- **Language** — en or es
|
||||||
|
- **Email Subject** — Optional custom subject
|
||||||
|
4. Output Variables:
|
||||||
|
- **Envelope ID** — Unique Docusign envelope ID
|
||||||
|
- **Success** — Boolean (true/false)
|
||||||
|
- **Error Message** — Error details if creation failed
|
||||||
|
5. Save and test the flow
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Authorization Issues
|
||||||
|
```bash
|
||||||
|
# If auth fails, try manual URL method:
|
||||||
|
sf org login web --alias dev-org --instance-url https://login.salesforce.com --json
|
||||||
|
|
||||||
|
# Copy the 'url' value and paste into your browser
|
||||||
|
```
|
||||||
|
|
||||||
|
### Deployment Errors
|
||||||
|
```bash
|
||||||
|
# Check what's in your org
|
||||||
|
sf project list metadata --target-org dev-org
|
||||||
|
|
||||||
|
# Validate without deploying
|
||||||
|
sf project validate deploy --target-org dev-org
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test Failures
|
||||||
|
```bash
|
||||||
|
# Run tests with verbose output
|
||||||
|
sf apex run test --target-org dev-org --result-format human --code-coverage -v
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Reference: Classes Deployed
|
||||||
|
|
||||||
|
**Apex Classes:**
|
||||||
|
- `DocusignCompositeEnvelopeBuilder` — Main invocable class (Flows)
|
||||||
|
- `DocusignEnvelopeRequestHandler` — Request validation & envelope building (reusable)
|
||||||
|
- `DocusignAPIService` — Docusign REST API wrapper
|
||||||
|
- `DocusignCredentials` — Custom settings singleton
|
||||||
|
|
||||||
|
**Test Classes:**
|
||||||
|
- `DocusignCompositeEnvelopeBuilderTest` — Tests for main invocable
|
||||||
|
- `DocusignEnvelopeRequestHandlerTest` — Tests for request handler (10 test methods)
|
||||||
|
- `DocusignAPIServiceTest` — Tests for API service
|
||||||
|
- `DocusignCredentialsTest` — Tests for credentials
|
||||||
|
|
||||||
|
**Custom Settings:**
|
||||||
|
- `Docusign_Configuration__c` — Stores Account ID and Base URL
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Next Steps After Successful Deploy
|
||||||
|
|
||||||
|
1. ✅ Verify all tests pass
|
||||||
|
2. ✅ Create Docusign Configuration in Salesforce
|
||||||
|
3. ✅ Set up Named Credential
|
||||||
|
4. ✅ Create a test Screen Flow
|
||||||
|
5. ✅ Test with real Docusign templates
|
||||||
|
6. ✅ Deploy to Production (when ready)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
For more details, see:
|
||||||
|
- `docs/deployment-guide.md` — Detailed deployment instructions
|
||||||
|
- `docs/api-reference.md` — API reference
|
||||||
|
- `docs/design.md` — Architecture & design decisions
|
||||||
|
|
@ -27,22 +27,11 @@ global with sharing class DocusignCompositeEnvelopeBuilder {
|
||||||
Result result = new Result();
|
Result result = new Result();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Validate inputs
|
// Validate request using handler
|
||||||
validateInputs(req);
|
DocusignEnvelopeRequestHandler.validateRequest(req);
|
||||||
|
|
||||||
// Remove duplicates and sort alphabetically
|
// Build envelope JSON using handler
|
||||||
List<String> sortedTemplateIds = sortTemplatesAlphabetically(
|
String envelopeJSON = DocusignEnvelopeRequestHandler.buildEnvelopeJSON(req);
|
||||||
new Set<String>(req.templateIds)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Build composite envelope JSON
|
|
||||||
String envelopeJSON = buildCompositeEnvelopeJSON(
|
|
||||||
sortedTemplateIds,
|
|
||||||
req.recordId,
|
|
||||||
req.language,
|
|
||||||
req.emailSubject,
|
|
||||||
null // customFields not supported in InvocableVariable (Phase 2 enhancement)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Get Docusign credentials
|
// Get Docusign credentials
|
||||||
DocusignCredentials creds = DocusignCredentials.getInstance();
|
DocusignCredentials creds = DocusignCredentials.getInstance();
|
||||||
|
|
@ -85,152 +74,6 @@ global with sharing class DocusignCompositeEnvelopeBuilder {
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Validates input parameters
|
|
||||||
* @param req Request object to validate
|
|
||||||
* @throws IllegalArgumentException if validation fails
|
|
||||||
*/
|
|
||||||
private static void validateInputs(Request 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');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for null template IDs
|
|
||||||
for (String templateId : req.templateIds) {
|
|
||||||
if (String.isBlank(templateId)) {
|
|
||||||
throw new IllegalArgumentException('Template ID cannot be blank');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Removes duplicates and sorts template IDs alphabetically
|
|
||||||
* @param templateIdSet Set of template IDs
|
|
||||||
* @return Sorted list of unique template IDs
|
|
||||||
*/
|
|
||||||
private static List<String> sortTemplatesAlphabetically(Set<String> templateIdSet) {
|
|
||||||
List<String> sortedList = new List<String>(templateIdSet);
|
|
||||||
sortedList.sort();
|
|
||||||
return sortedList;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Builds composite envelope JSON for Docusign API
|
|
||||||
* @param templateIds List of template IDs to combine
|
|
||||||
* @param recordId Salesforce record ID for custom fields
|
|
||||||
* @param language Language code (en/es)
|
|
||||||
* @param emailSubject Email subject line
|
|
||||||
* @param customFields Map of custom field name/value pairs
|
|
||||||
* @return JSON string for Docusign API request
|
|
||||||
*/
|
|
||||||
@TestVisible
|
|
||||||
private static String buildCompositeEnvelopeJSON(
|
|
||||||
List<String> templateIds,
|
|
||||||
String recordId,
|
|
||||||
String language,
|
|
||||||
String emailSubject,
|
|
||||||
Map<String, String> customFields
|
|
||||||
) {
|
|
||||||
// Build composite templates array
|
|
||||||
List<Object> compositeTemplates = new List<Object>();
|
|
||||||
|
|
||||||
Integer sequence = 1;
|
|
||||||
for (String templateId : templateIds) {
|
|
||||||
Map<String, Object> compositeTemplate = new Map<String, Object>{
|
|
||||||
'compositeTemplateId' => String.valueOf(sequence),
|
|
||||||
'serverTemplates' => new List<Object>{
|
|
||||||
new Map<String, Object>{
|
|
||||||
'sequence' => String.valueOf(sequence),
|
|
||||||
'templateId' => templateId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add custom fields if this is the first template
|
|
||||||
if (sequence == 1 && (String.isNotBlank(recordId) || customFields != null)) {
|
|
||||||
compositeTemplate.put('inlineTemplates', buildInlineTemplates(recordId, language, customFields));
|
|
||||||
}
|
|
||||||
|
|
||||||
compositeTemplates.add(compositeTemplate);
|
|
||||||
sequence++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build envelope object
|
|
||||||
Map<String, Object> envelope = new Map<String, Object>{
|
|
||||||
'status' => 'sent',
|
|
||||||
'emailSubject' => String.isNotBlank(emailSubject)
|
|
||||||
? emailSubject
|
|
||||||
: 'Please review and sign these forms',
|
|
||||||
'compositeTemplates' => compositeTemplates
|
|
||||||
};
|
|
||||||
|
|
||||||
return JSON.serialize(envelope);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Builds inline templates for custom fields
|
|
||||||
* @param recordId Salesforce record ID
|
|
||||||
* @param language Language code
|
|
||||||
* @param customFields Additional custom fields
|
|
||||||
* @return List of inline template objects
|
|
||||||
*/
|
|
||||||
private static List<Object> buildInlineTemplates(
|
|
||||||
String recordId,
|
|
||||||
String language,
|
|
||||||
Map<String, String> customFields
|
|
||||||
) {
|
|
||||||
List<Object> textCustomFields = new List<Object>();
|
|
||||||
|
|
||||||
// Add Salesforce record ID
|
|
||||||
if (String.isNotBlank(recordId)) {
|
|
||||||
textCustomFields.add(new Map<String, Object>{
|
|
||||||
'name' => 'SalesforceRecordId',
|
|
||||||
'value' => recordId,
|
|
||||||
'show' => 'false',
|
|
||||||
'required' => 'false'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add language
|
|
||||||
if (String.isNotBlank(language)) {
|
|
||||||
textCustomFields.add(new Map<String, Object>{
|
|
||||||
'name' => 'Language',
|
|
||||||
'value' => language,
|
|
||||||
'show' => 'false',
|
|
||||||
'required' => 'false'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add additional custom fields
|
|
||||||
if (customFields != null && !customFields.isEmpty()) {
|
|
||||||
for (String fieldName : customFields.keySet()) {
|
|
||||||
textCustomFields.add(new Map<String, Object>{
|
|
||||||
'name' => fieldName,
|
|
||||||
'value' => customFields.get(fieldName),
|
|
||||||
'show' => 'false',
|
|
||||||
'required' => 'false'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return new List<Object>{
|
|
||||||
new Map<String, Object>{
|
|
||||||
'sequence' => '1',
|
|
||||||
'customFields' => new Map<String, Object>{
|
|
||||||
'textCustomFields' => textCustomFields
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description Logs API call to debug log (future: custom object)
|
* @description Logs API call to debug log (future: custom object)
|
||||||
* @param templateCount Number of templates in envelope
|
* @param templateCount Number of templates in envelope
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,173 @@
|
||||||
|
/**
|
||||||
|
* @description Handles request validation and envelope JSON building 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(DocusignCompositeEnvelopeBuilder.Request 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');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for null template IDs
|
||||||
|
for (String templateId : req.templateIds) {
|
||||||
|
if (String.isBlank(templateId)) {
|
||||||
|
throw new IllegalArgumentException('Template ID cannot be blank');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Builds composite envelope JSON for Docusign API from request
|
||||||
|
* @param req Request object containing parameters
|
||||||
|
* @return JSON string for Docusign API request
|
||||||
|
*/
|
||||||
|
public static String buildEnvelopeJSON(DocusignCompositeEnvelopeBuilder.Request req) {
|
||||||
|
// Remove duplicates and sort alphabetically
|
||||||
|
List<String> sortedTemplateIds = sortTemplatesAlphabetically(
|
||||||
|
new Set<String>(req.templateIds)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Build composite envelope JSON
|
||||||
|
return buildCompositeEnvelopeJSON(
|
||||||
|
sortedTemplateIds,
|
||||||
|
req.recordId,
|
||||||
|
req.language,
|
||||||
|
req.emailSubject,
|
||||||
|
null // customFields not supported in InvocableVariable (Phase 2 enhancement)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Removes duplicates and sorts template IDs alphabetically
|
||||||
|
* @param templateIdSet Set of template IDs
|
||||||
|
* @return Sorted list of unique template IDs
|
||||||
|
*/
|
||||||
|
private static List<String> sortTemplatesAlphabetically(Set<String> templateIdSet) {
|
||||||
|
List<String> sortedList = new List<String>(templateIdSet);
|
||||||
|
sortedList.sort();
|
||||||
|
return sortedList;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Builds composite envelope JSON for Docusign API
|
||||||
|
* @param templateIds List of template IDs to combine
|
||||||
|
* @param recordId Salesforce record ID for custom fields
|
||||||
|
* @param language Language code (en/es)
|
||||||
|
* @param emailSubject Email subject line
|
||||||
|
* @param customFields Map of custom field name/value pairs
|
||||||
|
* @return JSON string for Docusign API request
|
||||||
|
*/
|
||||||
|
private static String buildCompositeEnvelopeJSON(
|
||||||
|
List<String> templateIds,
|
||||||
|
String recordId,
|
||||||
|
String language,
|
||||||
|
String emailSubject,
|
||||||
|
Map<String, String> customFields
|
||||||
|
) {
|
||||||
|
// Build composite templates array
|
||||||
|
List<Object> compositeTemplates = new List<Object>();
|
||||||
|
|
||||||
|
Integer sequence = 1;
|
||||||
|
for (String templateId : templateIds) {
|
||||||
|
Map<String, Object> compositeTemplate = new Map<String, Object>{
|
||||||
|
'compositeTemplateId' => String.valueOf(sequence),
|
||||||
|
'serverTemplates' => new List<Object>{
|
||||||
|
new Map<String, Object>{
|
||||||
|
'sequence' => String.valueOf(sequence),
|
||||||
|
'templateId' => templateId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add custom fields if this is the first template
|
||||||
|
if (sequence == 1 && (String.isNotBlank(recordId) || customFields != null)) {
|
||||||
|
compositeTemplate.put('inlineTemplates', buildInlineTemplates(recordId, language, customFields));
|
||||||
|
}
|
||||||
|
|
||||||
|
compositeTemplates.add(compositeTemplate);
|
||||||
|
sequence++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build envelope object
|
||||||
|
Map<String, Object> envelope = new Map<String, Object>{
|
||||||
|
'status' => 'sent',
|
||||||
|
'emailSubject' => String.isNotBlank(emailSubject)
|
||||||
|
? emailSubject
|
||||||
|
: 'Please review and sign these forms',
|
||||||
|
'compositeTemplates' => compositeTemplates
|
||||||
|
};
|
||||||
|
|
||||||
|
return JSON.serialize(envelope);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Builds inline templates for custom fields
|
||||||
|
* @param recordId Salesforce record ID
|
||||||
|
* @param language Language code
|
||||||
|
* @param customFields Additional custom fields
|
||||||
|
* @return List of inline template objects
|
||||||
|
*/
|
||||||
|
private static List<Object> buildInlineTemplates(
|
||||||
|
String recordId,
|
||||||
|
String language,
|
||||||
|
Map<String, String> customFields
|
||||||
|
) {
|
||||||
|
List<Object> textCustomFields = new List<Object>();
|
||||||
|
|
||||||
|
// Add Salesforce record ID
|
||||||
|
if (String.isNotBlank(recordId)) {
|
||||||
|
textCustomFields.add(new Map<String, Object>{
|
||||||
|
'name' => 'SalesforceRecordId',
|
||||||
|
'value' => recordId,
|
||||||
|
'show' => 'false',
|
||||||
|
'required' => 'false'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add language
|
||||||
|
if (String.isNotBlank(language)) {
|
||||||
|
textCustomFields.add(new Map<String, Object>{
|
||||||
|
'name' => 'Language',
|
||||||
|
'value' => language,
|
||||||
|
'show' => 'false',
|
||||||
|
'required' => 'false'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add additional custom fields
|
||||||
|
if (customFields != null && !customFields.isEmpty()) {
|
||||||
|
for (String fieldName : customFields.keySet()) {
|
||||||
|
textCustomFields.add(new Map<String, Object>{
|
||||||
|
'name' => fieldName,
|
||||||
|
'value' => customFields.get(fieldName),
|
||||||
|
'show' => 'false',
|
||||||
|
'required' => 'false'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new List<Object>{
|
||||||
|
new Map<String, Object>{
|
||||||
|
'sequence' => '1',
|
||||||
|
'customFields' => new Map<String, Object>{
|
||||||
|
'textCustomFields' => textCustomFields
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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,180 @@
|
||||||
|
/**
|
||||||
|
* @description Tests for DocusignEnvelopeRequestHandler
|
||||||
|
* @author Paul Huliganga
|
||||||
|
* @date 2026-02-25
|
||||||
|
*/
|
||||||
|
@isTest
|
||||||
|
public class DocusignEnvelopeRequestHandlerTest {
|
||||||
|
|
||||||
|
@isTest
|
||||||
|
static void testValidateRequest_Success() {
|
||||||
|
// Setup
|
||||||
|
DocusignCompositeEnvelopeBuilder.Request req = new DocusignCompositeEnvelopeBuilder.Request();
|
||||||
|
req.templateIds = new List<String>{ 'template1', 'template2' };
|
||||||
|
req.recordId = '001xx000003DHf';
|
||||||
|
req.language = 'en';
|
||||||
|
req.emailSubject = 'Test Subject';
|
||||||
|
|
||||||
|
// Test
|
||||||
|
Test.startTest();
|
||||||
|
DocusignEnvelopeRequestHandler.validateRequest(req);
|
||||||
|
Test.stopTest();
|
||||||
|
|
||||||
|
// Verify - no exception thrown
|
||||||
|
Assert.isTrue(true, 'Validation should pass');
|
||||||
|
}
|
||||||
|
|
||||||
|
@isTest
|
||||||
|
static void testValidateRequest_NoTemplateIds() {
|
||||||
|
// Setup
|
||||||
|
DocusignCompositeEnvelopeBuilder.Request req = new DocusignCompositeEnvelopeBuilder.Request();
|
||||||
|
req.templateIds = new List<String>();
|
||||||
|
req.recordId = '001xx000003DHf';
|
||||||
|
|
||||||
|
// Test & Verify
|
||||||
|
Test.startTest();
|
||||||
|
try {
|
||||||
|
DocusignEnvelopeRequestHandler.validateRequest(req);
|
||||||
|
Assert.fail('Should throw IllegalArgumentException');
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
Assert.isTrue(e.getMessage().contains('At least one template ID'), 'Correct error message');
|
||||||
|
}
|
||||||
|
Test.stopTest();
|
||||||
|
}
|
||||||
|
|
||||||
|
@isTest
|
||||||
|
static void testValidateRequest_TooManyTemplates() {
|
||||||
|
// Setup
|
||||||
|
DocusignCompositeEnvelopeBuilder.Request req = new DocusignCompositeEnvelopeBuilder.Request();
|
||||||
|
req.templateIds = new List<String>();
|
||||||
|
for (Integer i = 0; i < 15; i++) {
|
||||||
|
req.templateIds.add('template' + i);
|
||||||
|
}
|
||||||
|
req.recordId = '001xx000003DHf';
|
||||||
|
|
||||||
|
// Test & Verify
|
||||||
|
Test.startTest();
|
||||||
|
try {
|
||||||
|
DocusignEnvelopeRequestHandler.validateRequest(req);
|
||||||
|
Assert.fail('Should throw IllegalArgumentException');
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
Assert.isTrue(e.getMessage().contains('Maximum 14 templates'), 'Correct error message');
|
||||||
|
}
|
||||||
|
Test.stopTest();
|
||||||
|
}
|
||||||
|
|
||||||
|
@isTest
|
||||||
|
static void testValidateRequest_NoRecordId() {
|
||||||
|
// Setup
|
||||||
|
DocusignCompositeEnvelopeBuilder.Request req = new DocusignCompositeEnvelopeBuilder.Request();
|
||||||
|
req.templateIds = new List<String>{ 'template1' };
|
||||||
|
req.recordId = '';
|
||||||
|
|
||||||
|
// Test & Verify
|
||||||
|
Test.startTest();
|
||||||
|
try {
|
||||||
|
DocusignEnvelopeRequestHandler.validateRequest(req);
|
||||||
|
Assert.fail('Should throw IllegalArgumentException');
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
Assert.isTrue(e.getMessage().contains('Salesforce record ID'), 'Correct error message');
|
||||||
|
}
|
||||||
|
Test.stopTest();
|
||||||
|
}
|
||||||
|
|
||||||
|
@isTest
|
||||||
|
static void testValidateRequest_BlankTemplateId() {
|
||||||
|
// Setup
|
||||||
|
DocusignCompositeEnvelopeBuilder.Request req = new DocusignCompositeEnvelopeBuilder.Request();
|
||||||
|
req.templateIds = new List<String>{ 'template1', '', 'template3' };
|
||||||
|
req.recordId = '001xx000003DHf';
|
||||||
|
|
||||||
|
// Test & Verify
|
||||||
|
Test.startTest();
|
||||||
|
try {
|
||||||
|
DocusignEnvelopeRequestHandler.validateRequest(req);
|
||||||
|
Assert.fail('Should throw IllegalArgumentException');
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
Assert.isTrue(e.getMessage().contains('Template ID cannot be blank'), 'Correct error message');
|
||||||
|
}
|
||||||
|
Test.stopTest();
|
||||||
|
}
|
||||||
|
|
||||||
|
@isTest
|
||||||
|
static void testBuildEnvelopeJSON_Success() {
|
||||||
|
// Setup
|
||||||
|
DocusignCompositeEnvelopeBuilder.Request req = new DocusignCompositeEnvelopeBuilder.Request();
|
||||||
|
req.templateIds = new List<String>{ 'template2', 'template1', 'template2' }; // duplicates and unsorted
|
||||||
|
req.recordId = '001xx000003DHf';
|
||||||
|
req.language = 'es';
|
||||||
|
req.emailSubject = 'Custom Subject';
|
||||||
|
|
||||||
|
// Test
|
||||||
|
Test.startTest();
|
||||||
|
String json = DocusignEnvelopeRequestHandler.buildEnvelopeJSON(req);
|
||||||
|
Test.stopTest();
|
||||||
|
|
||||||
|
// Verify
|
||||||
|
Assert.isNotNull(json, 'JSON should be generated');
|
||||||
|
Assert.isTrue(json.contains('sent'), 'Should contain sent status');
|
||||||
|
Assert.isTrue(json.contains('Custom Subject'), 'Should contain custom subject');
|
||||||
|
Assert.isTrue(json.contains('compositeTemplates'), 'Should contain compositeTemplates');
|
||||||
|
}
|
||||||
|
|
||||||
|
@isTest
|
||||||
|
static void testBuildEnvelopeJSON_DefaultSubject() {
|
||||||
|
// Setup
|
||||||
|
DocusignCompositeEnvelopeBuilder.Request req = new DocusignCompositeEnvelopeBuilder.Request();
|
||||||
|
req.templateIds = new List<String>{ 'template1' };
|
||||||
|
req.recordId = '001xx000003DHf';
|
||||||
|
req.emailSubject = null;
|
||||||
|
|
||||||
|
// Test
|
||||||
|
Test.startTest();
|
||||||
|
String json = DocusignEnvelopeRequestHandler.buildEnvelopeJSON(req);
|
||||||
|
Test.stopTest();
|
||||||
|
|
||||||
|
// Verify
|
||||||
|
Assert.isTrue(json.contains('Please review and sign these forms'), 'Should use default subject');
|
||||||
|
}
|
||||||
|
|
||||||
|
@isTest
|
||||||
|
static void testBuildEnvelopeJSON_TemplatesAreSorted() {
|
||||||
|
// Setup
|
||||||
|
DocusignCompositeEnvelopeBuilder.Request req = new DocusignCompositeEnvelopeBuilder.Request();
|
||||||
|
req.templateIds = new List<String>{ 'template3', 'template1', 'template2' };
|
||||||
|
req.recordId = '001xx000003DHf';
|
||||||
|
|
||||||
|
// Test
|
||||||
|
Test.startTest();
|
||||||
|
String json = DocusignEnvelopeRequestHandler.buildEnvelopeJSON(req);
|
||||||
|
Test.stopTest();
|
||||||
|
|
||||||
|
// Verify - templates should be in sorted order (template1, template2, template3)
|
||||||
|
Integer idx1 = json.indexOf('template1');
|
||||||
|
Integer idx2 = json.indexOf('template2');
|
||||||
|
Integer idx3 = json.indexOf('template3');
|
||||||
|
Assert.isTrue(idx1 < idx2 && idx2 < idx3, 'Templates should be sorted alphabetically');
|
||||||
|
}
|
||||||
|
|
||||||
|
@isTest
|
||||||
|
static void testBuildEnvelopeJSON_DuplicatesRemoved() {
|
||||||
|
// Setup
|
||||||
|
DocusignCompositeEnvelopeBuilder.Request req = new DocusignCompositeEnvelopeBuilder.Request();
|
||||||
|
req.templateIds = new List<String>{ 'template1', 'template1', 'template2' };
|
||||||
|
req.recordId = '001xx000003DHf';
|
||||||
|
|
||||||
|
// Test
|
||||||
|
Test.startTest();
|
||||||
|
String json = DocusignEnvelopeRequestHandler.buildEnvelopeJSON(req);
|
||||||
|
Test.stopTest();
|
||||||
|
|
||||||
|
// Verify - should only have 2 compositeTemplate entries
|
||||||
|
Integer count = 0;
|
||||||
|
Integer pos = 0;
|
||||||
|
while ((pos = json.indexOf('compositeTemplateId', pos)) != -1) {
|
||||||
|
count++;
|
||||||
|
pos++;
|
||||||
|
}
|
||||||
|
Assert.areEqual(2, count, 'Should have 2 unique templates after deduplication');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
Loading…
Reference in New Issue