Initial commit: Salesforce Composite Envelope Builder

This commit is contained in:
Paul Huliganga 2026-02-25 09:22:29 -05:00
commit 4f734f0d17
38 changed files with 5247 additions and 0 deletions

21
.gitignore vendored Normal file
View File

@ -0,0 +1,21 @@
# Salesforce
.sfdx/
.sf/
node_modules/
dist/
*.zip
# IDEs
.vscode/
.idea/
*.swp
*.swo
# OS
.DS_Store
Thumbs.db
# Environment
.env
.env.local
.env.*.local

View File

@ -0,0 +1,12 @@
# List files or directories below to ignore them when running force:source:push, force:source:pull, and force:source:status
# More information: https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_exclude_source.htm
#
package.xml
# LWC configuration files
**/jsconfig.json
**/.eslintrc.json
# LWC Jest
**/__tests__/**

48
composite-envelope-builder/.gitignore vendored Normal file
View File

@ -0,0 +1,48 @@
# This file is used for Git repositories to specify intentionally untracked files that Git should ignore.
# If you are not using git, you can delete this file. For more information see: https://git-scm.com/docs/gitignore
# For useful gitignore templates see: https://github.com/github/gitignore
# Salesforce cache
.sf/
.sfdx/
.localdevserver/
deploy-options.json
# LWC VSCode autocomplete
**/lwc/jsconfig.json
# LWC Jest coverage reports
coverage/
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Dependency directories
node_modules/
# ApexPMD cache
.pmdCache
# Eslint cache
.eslintcache
# MacOS system files
.DS_Store
# Windows system files
Thumbs.db
ehthumbs.db
[Dd]esktop.ini
$RECYCLE.BIN/
# Local environment variables
.env
# Python Salesforce Functions
**/__pycache__/
**/.venv/
**/venv/

View File

@ -0,0 +1 @@
npm run precommit

View File

@ -0,0 +1,11 @@
# List files or directories below to ignore them when running prettier
# More information: https://prettier.io/docs/en/ignore.html
#
**/staticresources/**
.localdevserver
.sfdx
.sf
.vscode
coverage/

View File

@ -0,0 +1,17 @@
{
"trailingComma": "none",
"plugins": [
"prettier-plugin-apex",
"@prettier/plugin-xml"
],
"overrides": [
{
"files": "**/lwc/**/*.html",
"options": { "parser": "lwc" }
},
{
"files": "*.{cmp,page,component}",
"options": { "parser": "html" }
}
]
}

View File

@ -0,0 +1,248 @@
# Quick Start Guide
## ✅ Phase 2 Complete: Code Development
All Apex classes and tests have been created! Here's what you have:
### 📦 Code Files Created (6 classes)
**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
**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
**Total:** 39 test methods for comprehensive coverage!
**Metadata:**
- Custom Setting: `Docusign_Configuration__c` with `Account_Id__c` and `Base_URL__c` fields
- All `.cls-meta.xml` files (API version 61.0)
---
## 🚀 Next Steps (Deployment)
### Step 1: Open Project in VS Code
```bash
cd ~/.openclaw/workspace/projects/salesforce-composite-envelope-builder/composite-envelope-builder
code .
```
### Step 2: Install Salesforce Extension Pack (if not already installed)
In VS Code:
1. Open Extensions (Ctrl+Shift+X)
2. Search for "Salesforce Extension Pack"
3. Click Install
### Step 3: Authorize Your Salesforce Org
**For Sandbox:**
```bash
sf org login web --alias my-sandbox --instance-url https://test.salesforce.com
```
**For Production:**
```bash
sf org login web --alias my-production --instance-url https://login.salesforce.com
```
This will open a browser window. Log in with your Salesforce credentials.
### Step 4: Set Default Org
```bash
sf config set target-org my-sandbox
```
### Step 5: Deploy to Sandbox
```bash
sf project deploy start --target-org my-sandbox
```
Expected output:
```
Deploying v61.0 metadata to my-sandbox
Status: Succeeded
Component Deployed:
ApexClass DocusignCompositeEnvelopeBuilder
ApexClass DocusignAPIService
ApexClass DocusignCredentials
ApexClass DocusignCompositeEnvelopeBuilderTest
ApexClass DocusignAPIServiceTest
ApexClass DocusignCredentialsTest
CustomObject Docusign_Configuration__c
```
### Step 6: Run Unit Tests
```bash
sf apex run test --wait 10 --result-format human --code-coverage --target-org my-sandbox
```
Expected output:
```
=== Test Summary ===
Outcome: Passed
Tests Ran: 39
Passing: 39
Failing: 0
Skipped: 0
Pass Rate: 100%
Code Coverage: 85%+
```
### Step 7: Configure Docusign Credentials
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)
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
3. **Add Remote Site Settings**
- Setup → Remote Site Settings → New Remote Site
- Name: `Docusign_API`
- URL: `https://na3.docusign.net`
- Active: ✓
### Step 8: Update Screen Flow
See `docs/deployment-guide.md` section 5 for detailed Flow configuration steps.
**Summary:**
1. Open your existing form selection Flow
2. Add Apex Action element (after template selection)
3. Select action: `Send Composite Docusign Envelope`
4. Map inputs:
- `templateIds``{!SelectedTemplateIds}` (from checkboxes)
- `recordId``{!recordId}`
- `language``{!SelectedLanguage}`
- `emailSubject` → "Please review and sign these forms"
5. Store outputs:
- `envelopeId``{!EnvelopeId}`
- `success``{!Success}`
- `errorMessage``{!ErrorMessage}`
6. Add Decision element for success/error handling
7. Save & Activate
### Step 9: Test Integration
1. Navigate to Salesforce record
2. Launch Screen Flow
3. Select language: English
4. Check 2-3 templates
5. Click Send
6. Verify:
- Success message shown
- Envelope ID displayed
- Check Docusign: envelope created with multiple documents
- Recipients receive ONE email
---
## 📚 Documentation
All docs are in the `docs/` folder:
- **requirements.md** - What we're building and why
- **design.md** - Architecture, sequence diagrams, data flow
- **api-reference.md** - Docusign REST API details
- **deployment-guide.md** - Complete deployment instructions
---
## 🧪 Test Coverage Details
**DocusignCompositeEnvelopeBuilderTest** (12 methods):
- ✅ Success with 3 templates
- ✅ Single template (minimum)
- ✅ 14 templates (maximum)
- ✅ Duplicate template handling
- ✅ Custom fields support
- ✅ Validation: no templates
- ✅ Validation: too many templates (>14)
- ✅ Validation: missing record ID
- ✅ API error 400 (bad request)
- ✅ API error 401 (unauthorized)
- ✅ JSON builder test
**DocusignAPIServiceTest** (14 methods):
- ✅ Successful envelope creation
- ✅ Parse envelope ID
- ✅ Parse envelope ID missing
- ✅ Handle errors: 400, 401, 403, 404, 429, 500
- ✅ Retry logic (transient failures)
- ✅ Build HTTP request
- ✅ Execute with retry (non-retryable error)
**DocusignCredentialsTest** (13 methods):
- ✅ Singleton pattern
- ✅ Load from Custom Settings
- ✅ Get access token, account ID, base URL
- ✅ Validate success
- ✅ Validate failures: missing account ID, base URL, token
- ✅ Set test credentials
- ✅ Reset instance
- ✅ Missing Custom Settings error
- ✅ Named Credential format
**Total: 39 test methods across 6 classes**
---
## 🎯 Project Status
| Phase | Status |
|-------|--------|
| Requirements & Design | ✅ Complete |
| Apex Code Development | ✅ Complete |
| Unit Tests | ✅ Complete (39 tests) |
| Documentation | ✅ Complete (56+ KB) |
| Deployment | ⏳ Ready to deploy |
| Flow Integration | ⏳ Awaiting deployment |
| Production Testing | ⏳ Next |
---
## 💡 Tips
**Before Deploying:**
- Review `docs/deployment-guide.md` sections 2-3 for Docusign setup
- Have your Docusign Integration Key ready
- Know your Docusign Account ID
**After Deploying:**
- Run all tests to verify 100% pass rate
- Check debug logs for any issues
- Test with 1 template first, then multiple
**Flow Modification:**
- Keep your existing template selection UI
- Replace the loop that sends individual envelopes
- Add the Apex Action in place of the loop
---
## ❓ Need Help?
Check the troubleshooting section in `docs/deployment-guide.md` (section 10) for common issues and solutions.
---
**Project Created:** February 23, 2026
**Developer:** Paul Huliganga
**Ready to Deploy:** YES! 🚀

View File

@ -0,0 +1,105 @@
# Salesforce Composite Envelope Builder
## Project Overview
A Salesforce solution for sending multiple Docusign templates in a single envelope using composite template functionality via the Docusign REST API.
## Problem Statement
Current implementation requires 42+ pre-built Docusign templates for various combinations of 14 forms (English + Spanish). This approach is:
- Difficult to maintain
- Cannot handle all possible combinations
- Requires creating new templates for each scenario
## Solution
Build custom Apex code to dynamically combine multiple single-form templates into one envelope at runtime, allowing users to select any combination of forms.
## Key Features
- **Dynamic Form Selection**: Users select forms via Salesforce Screen Flow checkboxes
- **Single Template Per Form**: 28 templates total (14 forms × 2 languages)
- **Composite Envelope API**: Combines templates into one envelope via Docusign REST API
- **Alphabetical Ordering**: Forms presented and combined in alphabetical order by title
- **Automatic Recipient Merge**: Recipients/roles automatically merged across templates
- **Salesforce Integration**: Documents written back to initiating Salesforce record
## Technical Architecture
### Components
1. **Salesforce Screen Flow** (existing, minor modifications)
- Language selection
- Template display and selection (checkboxes)
- Calls Apex action with selected template IDs
2. **Custom Apex Class** (new)
- `DocusignCompositeEnvelopeBuilder` - Invocable method for Flow integration
- Constructs composite envelope JSON
- Makes Docusign REST API callout
- Returns envelope ID to Flow
3. **Unit Tests** (new)
- Test class with >75% code coverage
- Mock callouts for Docusign API
- Edge case handling
### Technologies
- **Salesforce Platform**: Apex, Screen Flows, Lightning Web Components (potential future UI)
- **Docusign REST API v2.1**: Composite Templates endpoint
- **VS Code + Salesforce Extensions**: Development environment
## Project Structure
```
salesforce-composite-envelope-builder/
├── docs/
│ ├── requirements.md # Detailed requirements
│ ├── design.md # Architecture and design decisions
│ ├── api-reference.md # Docusign API integration details
│ └── deployment-guide.md # Deployment and configuration
├── force-app/main/default/
│ ├── classes/
│ │ ├── DocusignCompositeEnvelopeBuilder.cls
│ │ ├── DocusignCompositeEnvelopeBuilder.cls-meta.xml
│ │ ├── DocusignCompositeEnvelopeBuilderTest.cls
│ │ ├── DocusignCompositeEnvelopeBuilderTest.cls-meta.xml
│ │ ├── DocusignCredentials.cls
│ │ └── DocusignCredentials.cls-meta.xml
│ └── flows/
│ └── Form_Selection_Flow.flow-meta.xml (example/reference)
├── scripts/
│ └── apex/
│ └── test-composite-envelope.apex # Manual testing script
└── README.md # This file
```
## Getting Started
See [docs/deployment-guide.md](docs/deployment-guide.md) for setup instructions.
## Documentation
- [Requirements](docs/requirements.md) - Functional and technical requirements
- [Design](docs/design.md) - Architecture, sequence diagrams, data flow
- [API Reference](docs/api-reference.md) - Docusign REST API integration details
- [Deployment Guide](docs/deployment-guide.md) - Installation and configuration
## Timeline
- **Phase 1**: Requirements & Design Documentation ✅
- **Phase 2**: Apex Class Development
- **Phase 3**: Unit Tests & Code Coverage
- **Phase 4**: Flow Integration
- **Phase 5**: Testing & Deployment
## License
Internal project for customer implementation.
---
**Last Updated**: February 23, 2026
**Project Lead**: Paul Huliganga
**Status**: In Development

View File

@ -0,0 +1,13 @@
{
"orgName": "paulh company",
"edition": "Developer",
"features": ["EnableSetPasswordInApi"],
"settings": {
"lightningExperienceSettings": {
"enableS1DesktopEnabled": true
},
"mobileSettings": {
"enableS1EncryptedStoragePref2": false
}
}
}

View File

@ -0,0 +1,104 @@
#!/bin/bash
# Deploy to Salesforce Developer Edition (WSL-friendly)
echo "🚀 Deploying to Developer Edition Org (WSL Mode)"
echo "================================================"
echo ""
# Detect Windows Chrome path
CHROME_PATHS=(
"/mnt/c/Program Files/Google/Chrome/Application/chrome.exe"
"/mnt/c/Program Files (x86)/Google/Chrome/Application/chrome.exe"
"/mnt/c/Program Files/Microsoft/Edge/Application/msedge.exe"
"/mnt/c/Program Files (x86)/Microsoft/Edge/Application/msedge.exe"
)
BROWSER=""
for path in "${CHROME_PATHS[@]}"; do
if [ -f "$path" ]; then
BROWSER="$path"
echo "✅ Found browser: $BROWSER"
break
fi
done
if [ -z "$BROWSER" ]; then
echo "⚠️ Could not auto-detect browser."
echo " Please specify Chrome or Edge path:"
echo " Example: /mnt/c/Program Files/Google/Chrome/Application/chrome.exe"
read -p "Browser path: " BROWSER
if [ ! -f "$BROWSER" ]; then
echo "❌ Browser not found at: $BROWSER"
echo ""
echo "Alternative: Use manual authentication URL method"
echo "Run this command:"
echo " sf org login web --alias dev-org --instance-url https://login.salesforce.com --json"
echo ""
echo "Then copy the URL from the output and open it in your Windows browser."
exit 1
fi
fi
echo ""
echo "🔐 Authorizing your Developer Edition org..."
echo " Opening browser: $(basename "$BROWSER")"
echo ""
sf org login web --alias dev-org --instance-url https://login.salesforce.com --browser "$BROWSER"
if [ $? -ne 0 ]; then
echo ""
echo "❌ Authorization failed."
echo ""
echo "📝 Manual alternative:"
echo " 1. Run: sf org login web --alias dev-org --instance-url https://login.salesforce.com --json"
echo " 2. Copy the 'url' from the JSON output"
echo " 3. Paste it into your Windows browser"
echo " 4. Log in and authorize"
echo " 5. Re-run this script"
exit 1
fi
echo ""
echo "✅ Authorization successful"
echo ""
# Set as default
sf config set target-org dev-org
# Deploy
echo "📦 Deploying code to Developer Edition..."
sf project deploy start --target-org dev-org
if [ $? -ne 0 ]; then
echo "❌ Deployment failed. Check errors above."
exit 1
fi
echo ""
echo "✅ Deployment successful!"
echo ""
# Run tests
echo "🧪 Running all 39 unit tests..."
sf apex run test --wait 10 --result-format human --code-coverage --target-org dev-org
echo ""
echo "======================================"
echo "✅ Deployment to Developer Edition Complete!"
echo ""
echo "📝 Next Steps:"
echo "1. Log into your Developer Edition org"
echo "2. Setup → Custom Settings → Docusign Configuration → Manage → New"
echo " - Account Id: {your Docusign sandbox account ID}"
echo " - Base URL: callout:DocusignAPI"
echo ""
echo "3. Setup → Named Credentials → New Named Credential"
echo " - Name: DocusignAPI"
echo " - URL: https://demo.docusign.net/restapi/v2.1"
echo " - Configure OAuth for Docusign Sandbox"
echo ""
echo "4. Create/update your Screen Flow to call the Apex Action"
echo ""
echo "See docs/deployment-guide.md for detailed configuration."

View File

@ -0,0 +1,91 @@
#!/bin/bash
# Deployment script for Salesforce Composite Envelope Builder
echo "🚀 Salesforce Composite Envelope Builder Deployment"
echo "=================================================="
echo ""
# Check if sf CLI is installed
if ! command -v sf &> /dev/null; then
echo "❌ Salesforce CLI not found. Please install it first:"
echo " npm install -g @salesforce/cli"
exit 1
fi
echo "✅ Salesforce CLI found: $(sf --version)"
echo ""
# Ask for org type
echo "Which org do you want to deploy to?"
echo "1) Sandbox"
echo "2) Production"
read -p "Enter choice (1 or 2): " ORG_CHOICE
if [ "$ORG_CHOICE" == "1" ]; then
ORG_TYPE="sandbox"
INSTANCE_URL="https://test.salesforce.com"
ORG_ALIAS="composite-envelope-sandbox"
elif [ "$ORG_CHOICE" == "2" ]; then
ORG_TYPE="production"
INSTANCE_URL="https://login.salesforce.com"
ORG_ALIAS="composite-envelope-production"
else
echo "❌ Invalid choice. Exiting."
exit 1
fi
echo ""
echo "📝 Deploying to: $ORG_TYPE"
echo "🔗 Instance URL: $INSTANCE_URL"
echo "🏷️ Org Alias: $ORG_ALIAS"
echo ""
# Authorize org
echo "🔐 Authorizing org..."
sf org login web --alias "$ORG_ALIAS" --instance-url "$INSTANCE_URL"
if [ $? -ne 0 ]; then
echo "❌ Authorization failed. Exiting."
exit 1
fi
echo "✅ Authorization successful"
echo ""
# Set default org
echo "⚙️ Setting default org..."
sf config set target-org "$ORG_ALIAS"
echo ""
echo "📦 Deploying metadata..."
sf project deploy start --target-org "$ORG_ALIAS"
if [ $? -ne 0 ]; then
echo "❌ Deployment failed. Check errors above."
exit 1
fi
echo ""
echo "✅ Deployment successful!"
echo ""
# Run tests
echo "🧪 Running unit tests..."
sf apex run test --wait 10 --result-format human --code-coverage --target-org "$ORG_ALIAS"
if [ $? -ne 0 ]; then
echo "⚠️ Some tests failed. Please review."
else
echo "✅ All tests passed!"
fi
echo ""
echo "=================================================="
echo "✅ Deployment Complete!"
echo ""
echo "Next steps:"
echo "1. Configure Docusign credentials in Salesforce"
echo "2. Update your Screen Flow to call the Apex Action"
echo "3. Test with real Docusign templates"
echo ""
echo "See docs/deployment-guide.md for detailed instructions."

View File

@ -0,0 +1,529 @@
# API Reference
**Project**: Salesforce Composite Envelope Builder
**Version**: 1.0
**Date**: February 23, 2026
---
## 1. Docusign REST API Integration
### 1.1 Base Endpoint
**Production**:
```
https://na3.docusign.net/restapi/v2.1
```
**Sandbox**:
```
https://demo.docusign.net/restapi/v2.1
```
**Note**: Replace `na3` with your account's data center (na2, na3, eu1, etc.)
---
## 2. Create Composite Envelope
### 2.1 HTTP Request
**Method**: POST
**Endpoint**: `/accounts/{accountId}/envelopes`
**Content-Type**: `application/json`
**Authorization**: `Bearer {access_token}`
### 2.2 Request Headers
```http
POST /restapi/v2.1/accounts/12345678-abcd-1234-abcd-1234567890ab/envelopes HTTP/1.1
Host: na3.docusign.net
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6IjY4MTg...
Content-Type: application/json
Accept: application/json
```
### 2.3 Request Body (Composite Templates)
#### Minimum Example (2 templates)
```json
{
"status": "sent",
"emailSubject": "Please review and sign these forms",
"compositeTemplates": [
{
"compositeTemplateId": "1",
"serverTemplates": [
{
"sequence": "1",
"templateId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}
]
},
{
"compositeTemplateId": "2",
"serverTemplates": [
{
"sequence": "2",
"templateId": "b2c3d4e5-f6g7-8901-bcde-fg2345678901"
}
]
}
]
}
```
#### Advanced Example (with merge fields)
```json
{
"status": "sent",
"emailSubject": "Please review and sign these forms",
"compositeTemplates": [
{
"compositeTemplateId": "1",
"serverTemplates": [
{
"sequence": "1",
"templateId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}
],
"inlineTemplates": [
{
"sequence": "1",
"customFields": {
"textCustomFields": [
{
"name": "SalesforceRecordId",
"value": "0018V00000ABC123"
},
{
"name": "CaseNumber",
"value": "00001234"
}
]
}
}
]
}
]
}
```
### 2.4 Response
#### Success (201 Created)
```json
{
"envelopeId": "f9876543-21ab-cdef-0123-456789abcdef",
"uri": "/envelopes/f9876543-21ab-cdef-0123-456789abcdef",
"statusDateTime": "2026-02-23T12:34:56.789Z",
"status": "sent"
}
```
#### Error (400 Bad Request)
```json
{
"errorCode": "INVALID_REQUEST_PARAMETER",
"message": "The request contained at least one invalid parameter. The template with ID 'invalid-id' does not exist."
}
```
#### Error (401 Unauthorized)
```json
{
"errorCode": "USER_AUTHENTICATION_FAILED",
"message": "One or both of Username and Password are invalid."
}
```
---
## 3. Composite Template Structure
### 3.1 Key Concepts
**compositeTemplates** (array):
- Each element represents a server template to include in the envelope
- Order in array determines document order in envelope
**compositeTemplateId** (string):
- Unique identifier for this composite template within the request
- Can be any string (recommend using sequence numbers: "1", "2", "3")
**serverTemplates** (array):
- References pre-existing templates in your Docusign account
- Must include `templateId` (GUID from Docusign)
**sequence** (string):
- Determines document order within the envelope
- "1" = first document, "2" = second, etc.
- **Important**: Use strings, not integers
**inlineTemplates** (array, optional):
- Allows runtime override of template values
- Used for custom fields, recipient data, tabs
### 3.2 Recipient Merging
**Automatic Merge**:
When multiple templates have recipients with the same `roleName`, Docusign automatically merges them into a single recipient.
**Example**:
- Template A has recipient role: "Signer"
- Template B has recipient role: "Signer"
- Result: One recipient signs all documents
**Requirements for merge**:
- Exact match on `roleName`
- Same recipient `routingOrder` (signing order)
- Merge happens automatically, no additional configuration needed
---
## 4. Authentication
### 4.1 JWT (JSON Web Token)
**Preferred method for server-to-server integration**
#### Step 1: Generate JWT
```apex
// Apex pseudocode
String jwt = generateJWT(
integrationKey, // From Docusign Apps & Keys
userId, // Docusign user GUID
rsaPrivateKey, // RSA private key (PEM format)
'https://account-d.docusign.com/oauth/auth', // Sandbox
3600 // Token expiry (1 hour)
);
```
#### Step 2: Exchange JWT for Access Token
**Request**:
```http
POST /oauth/token HTTP/1.1
Host: account-d.docusign.com
Content-Type: application/x-www-form-urlencoded
grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion={jwt}
```
**Response**:
```json
{
"access_token": "eyJ0eXAiOiJNVCIsImFsZyI...",
"token_type": "Bearer",
"expires_in": 3600
}
```
#### Step 3: Use Access Token
```http
Authorization: Bearer eyJ0eXAiOiJNVCIsImFsZyI...
```
### 4.2 OAuth2 Authorization Code
**User-based authentication** (not recommended for this use case)
See [Docusign OAuth2 documentation](https://developers.docusign.com/platform/auth/authcode/) for details.
---
## 5. Error Codes
### 5.1 Common Error Codes
| Error Code | HTTP | Meaning | Resolution |
|------------|------|---------|------------|
| `INVALID_REQUEST_PARAMETER` | 400 | Invalid parameter in request | Check JSON structure, template IDs |
| `USER_AUTHENTICATION_FAILED` | 401 | Invalid or expired access token | Refresh access token |
| `USER_LACKS_PERMISSIONS` | 403 | User doesn't have permission | Check Docusign user permissions |
| `RESOURCE_NOT_FOUND` | 404 | Template ID not found | Verify template exists in account |
| `DUPLICATE_RESOURCE` | 409 | Duplicate request | Check for duplicate envelope |
| `ONESIGNAL_GENERIC_ERROR` | 500 | Docusign server error | Retry after delay |
### 5.2 Rate Limits
**Hourly limit**: Varies by plan (1,000 - 10,000+ API calls/hour)
**Response header when nearing limit**:
```http
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 50
X-RateLimit-Reset: 1614358800
```
**Error when limit exceeded**:
```json
{
"errorCode": "HOURLY_APIINVOCATION_LIMIT_EXCEEDED",
"message": "The maximum number of hourly API invocations has been exceeded."
}
```
**Mitigation**:
- Implement exponential backoff
- Cache access tokens (don't generate new token for each request)
- Batch operations when possible
---
## 6. Custom Fields
### 6.1 Text Custom Fields
**Use case**: Store Salesforce record ID in envelope
```json
{
"compositeTemplates": [
{
"inlineTemplates": [
{
"customFields": {
"textCustomFields": [
{
"name": "SalesforceRecordId",
"value": "0018V00000ABC123",
"show": "false",
"required": "false"
}
]
}
}
]
}
]
}
```
### 6.2 List Custom Fields
```json
{
"customFields": {
"listCustomFields": [
{
"name": "FormLanguage",
"value": "English",
"listItems": ["English", "Spanish"]
}
]
}
}
```
---
## 7. Webhook Configuration
### 7.1 Connect Webhook (for document retrieval)
**Not required for Phase 1**, but useful for future automation.
**Endpoint**: Configure in Docusign Admin → Connect → Add Configuration
**Webhook payload** (when envelope completes):
```json
{
"event": "envelope-completed",
"apiVersion": "v2.1",
"uri": "/restapi/v2.1/accounts/123/envelopes/abc",
"envelopeId": "f9876543-21ab-cdef-0123-456789abcdef",
"envelopeSummary": {
"status": "completed",
"emailSubject": "Please review and sign",
"completedDateTime": "2026-02-23T14:30:00Z"
}
}
```
---
## 8. Template Management APIs
### 8.1 List Templates
**Use case**: Retrieve template list for Flow dropdown
**Request**:
```http
GET /restapi/v2.1/accounts/{accountId}/templates?count=100&order=name&order_by=asc
```
**Response**:
```json
{
"resultSetSize": "14",
"totalSetSize": "14",
"envelopeTemplates": [
{
"templateId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"name": "Form A - English",
"shared": "true",
"description": "English version of Form A"
},
{
"templateId": "b2c3d4e5-f6g7-8901-bcde-fg2345678901",
"name": "Form B - English",
"shared": "true",
"description": "English version of Form B"
}
]
}
```
### 8.2 Get Template Details
**Use case**: Retrieve template metadata (recipients, tabs)
**Request**:
```http
GET /restapi/v2.1/accounts/{accountId}/templates/{templateId}
```
**Response**:
```json
{
"templateId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"name": "Form A - English",
"recipients": {
"signers": [
{
"roleName": "Signer 1",
"recipientId": "1",
"routingOrder": "1"
},
{
"roleName": "Signer 2",
"recipientId": "2",
"routingOrder": "2"
}
]
},
"documents": [
{
"documentId": "1",
"name": "Form_A.pdf",
"pages": "2"
}
]
}
```
---
## 9. Apex HTTP Callout Example
### 9.1 Complete Callout
```apex
public static String createCompositeEnvelope(String envelopeJSON, String accessToken, String accountId) {
HttpRequest req = new HttpRequest();
req.setEndpoint('callout:DocusignAPI/accounts/' + accountId + '/envelopes');
req.setMethod('POST');
req.setHeader('Authorization', 'Bearer ' + accessToken);
req.setHeader('Content-Type', 'application/json');
req.setHeader('Accept', 'application/json');
req.setBody(envelopeJSON);
req.setTimeout(120000); // 120 seconds
Http http = new Http();
HttpResponse res = http.send(req);
if (res.getStatusCode() == 201) {
Map<String, Object> responseMap = (Map<String, Object>) JSON.deserializeUntyped(res.getBody());
return (String) responseMap.get('envelopeId');
} else {
throw new CalloutException('Docusign API error [' + res.getStatusCode() + ']: ' + res.getBody());
}
}
```
### 9.2 Named Credential Configuration
**Setup → Named Credentials → New Named Credential**
**Settings**:
- **Label**: Docusign API
- **Name**: DocusignAPI
- **URL**: `https://na3.docusign.net/restapi/v2.1`
- **Identity Type**: Named Principal
- **Authentication Protocol**: OAuth 2.0
- **Scope**: `signature impersonation`
- **Token Endpoint**: `https://account-d.docusign.com/oauth/token` (sandbox)
- **JWT Token Exchange**: Enabled
**Usage in Apex**:
```apex
req.setEndpoint('callout:DocusignAPI/accounts/' + accountId + '/envelopes');
```
---
## 10. Testing with Postman
### 10.1 Import Docusign Collection
Docusign provides an official Postman collection:
https://github.com/docusign/postman-collections
### 10.2 Create Composite Envelope Test
**Request**:
```
POST {{baseUrl}}/accounts/{{accountId}}/envelopes
Headers:
Authorization: Bearer {{accessToken}}
Content-Type: application/json
Body:
{
"status": "sent",
"emailSubject": "Test Composite Envelope",
"compositeTemplates": [
{
"compositeTemplateId": "1",
"serverTemplates": [
{
"sequence": "1",
"templateId": "{{templateId1}}"
}
]
},
{
"compositeTemplateId": "2",
"serverTemplates": [
{
"sequence": "2",
"templateId": "{{templateId2}}"
}
]
}
]
}
```
---
## 11. Reference Links
- [Docusign REST API Reference](https://developers.docusign.com/docs/esign-rest-api/reference/)
- [Composite Templates Guide](https://developers.docusign.com/docs/esign-rest-api/how-to/request-signature-composite-template/)
- [JWT Authentication](https://developers.docusign.com/platform/auth/jwt/)
- [Salesforce Named Credentials](https://help.salesforce.com/s/articleView?id=sf.named_credentials_about.htm)
- [Salesforce HTTP Callouts](https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_classes_restful_http_httprequest.htm)
---
**Document Version**: 1.0
**Last Updated**: February 23, 2026

View File

@ -0,0 +1,531 @@
# Deployment Guide
**Project**: Salesforce Composite Envelope Builder
**Version**: 1.0
**Date**: February 23, 2026
---
## 1. Prerequisites
### 1.1 Salesforce Requirements
- **Edition**: Enterprise Edition or higher (required for API access)
- **User Permissions**:
- API Enabled
- Modify All Data (for deployment)
- Manage Flows (for Flow updates)
- **Features Enabled**:
- Screen Flows
- Apex Classes
### 1.2 Docusign Requirements
- **Account**: Docusign eSignature (Production or Sandbox)
- **API Access**: Enabled
- **Templates**: 28 single-form templates created (14 English + 14 Spanish)
- **User Role**: Sender or higher
### 1.3 Development Tools
- **VS Code** with Salesforce Extension Pack
- **Salesforce CLI** (sf CLI version 2.0+)
- **Git** for version control
---
## 2. Docusign Configuration
### 2.1 Create Integration Key
**Step 1: Navigate to Apps & Keys**
1. Log in to Docusign
2. Go to **Settings → Apps and Keys**
3. Click **Add App and Integration Key**
**Step 2: Configure Integration**
- **App Name**: Salesforce Composite Envelope Builder
- **Integration Type**: JWT (JSON Web Token)
- **Redirect URI**: Not required for JWT
- **Secret Key**: Save securely (will be shown once)
**Step 3: Generate RSA Key Pair**
1. Click **Generate RSA**
2. Download private key (PEM file)
3. Copy public key (will be added to Salesforce)
**Step 4: Grant Consent**
1. Click **Grant Consent** button
2. Authenticate as Docusign admin user
3. Accept permissions
**Step 5: Note Important Values**
- **Integration Key** (GUID): e.g., `a1b2c3d4-...`
- **User ID** (GUID of API user): e.g., `f9876543-...`
- **Account ID** (GUID): e.g., `12345678-...`
- **Base URL**: e.g., `https://na3.docusign.net/restapi/v2.1`
### 2.2 Create Templates
**For each of 14 forms, create 2 templates (English + Spanish)**:
1. Go to **Templates → New → Use a Template**
2. Upload PDF document
3. Add fields (signature tabs, text fields, etc.)
4. Define recipients:
- **Role 1**: Signer 1 (routing order 1)
- **Role 2**: Signer 2 (routing order 2)
5. Name template clearly:
- Format: `[Form Name] - [Language]`
- Example: `Form A - English`, `Form A - Spanish`
6. Save template
7. Note template ID (GUID)
**Template Naming Convention** (for alphabetical sorting):
```
Form A - Credit Application - English
Form A - Credit Application - Spanish
Form B - Income Verification - English
Form B - Income Verification - Spanish
... (continue alphabetically)
```
---
## 3. Salesforce Configuration
### 3.1 Create Named Credential
**Option 1: External Credential (Recommended for JWT)**
**Step 1: Create External Credential**
1. Setup → Named Credentials → External Credentials → New
2. **Label**: Docusign JWT Auth
3. **Name**: DocusignJWTAuth
4. **Authentication Protocol**: Custom
5. **Identity Type**: Named Principal
6. Click **Create**
**Step 2: Add Custom Headers**
1. Open External Credential → Custom Headers → New
2. Add header:
- **Header Field Name**: Authorization
- **Header Field Value**: `Bearer {!$Credential.DocusignJWTAuth.AccessToken}`
**Step 3: Create Named Credential**
1. Setup → Named Credentials → New Named Credential
2. **Label**: Docusign API
3. **Name**: DocusignAPI
4. **URL**: `https://na3.docusign.net/restapi/v2.1` (or your data center)
5. **External Credential**: DocusignJWTAuth
6. **Enabled for Callouts**: Checked
7. Click **Save**
---
**Option 2: Legacy Named Credential (Simpler but less flexible)**
1. Setup → Named Credentials → New Legacy
2. **Label**: Docusign API
3. **Name**: DocusignAPI
4. **URL**: `https://na3.docusign.net/restapi/v2.1`
5. **Identity Type**: Named Principal
6. **Authentication Protocol**: OAuth 2.0
7. **Authentication Provider**: (Create new - see below)
8. **Scope**: `signature impersonation`
9. **Save**
**Create Authentication Provider**:
1. Setup → Auth. Providers → New
2. **Provider Type**: Open ID Connect
3. **Name**: Docusign OAuth
4. **Consumer Key**: {Integration Key from Docusign}
5. **Consumer Secret**: {Secret from Docusign}
6. **Authorize Endpoint URL**: `https://account-d.docusign.com/oauth/auth`
7. **Token Endpoint URL**: `https://account-d.docusign.com/oauth/token`
8. **Default Scopes**: `signature impersonation`
9. **Save**
### 3.2 Create Custom Settings (for Account ID)
**Step 1: Create Custom Setting**
1. Setup → Custom Settings → New
2. **Label**: Docusign Configuration
3. **Object Name**: Docusign_Configuration
4. **Setting Type**: Hierarchy
5. Add custom fields:
- **Account_Id__c** (Text, 255)
- **Base_URL__c** (Text, 255)
6. **Save**
**Step 2: Manage Custom Setting**
1. Setup → Custom Settings → Docusign Configuration → Manage
2. Click **New** (organization-wide default)
3. **Account Id**: {Docusign Account GUID}
4. **Base URL**: `https://na3.docusign.net/restapi/v2.1`
5. **Save**
### 3.3 Create Remote Site Settings
1. Setup → Remote Site Settings → New Remote Site
2. **Remote Site Name**: Docusign_API
3. **Remote Site URL**: `https://na3.docusign.net`
4. **Active**: Checked
5. **Save**
**Add additional sites** (if using sandbox or different data center):
- `https://demo.docusign.net` (Docusign Sandbox)
- `https://account-d.docusign.com` (OAuth endpoints)
---
## 4. Code Deployment
### 4.1 Clone Repository (if using Git)
```bash
git clone https://github.com/your-org/salesforce-composite-envelope-builder.git
cd salesforce-composite-envelope-builder/composite-envelope-builder
```
### 4.2 Authorize Salesforce Org
**Sandbox**:
```bash
sf org login web --alias my-sandbox --instance-url https://test.salesforce.com
```
**Production**:
```bash
sf org login web --alias my-production --instance-url https://login.salesforce.com
```
### 4.3 Deploy to Sandbox
```bash
sf project deploy start --target-org my-sandbox
```
**Expected output**:
```
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
```
### 4.4 Run Unit Tests
```bash
sf apex run test --class-names DocusignCompositeEnvelopeBuilderTest --target-org my-sandbox --wait 5 --result-format human
```
**Expected output**:
```
Test Success 1 tests ran, 1 passed, 0 failed, 0 skipped
Code Coverage 85% (target: 75%)
```
### 4.5 Validate Production (without deploying)
```bash
sf project deploy validate --target-org my-production --test-level RunLocalTests
```
### 4.6 Deploy to Production
```bash
sf project deploy start --target-org my-production --test-level RunLocalTests
```
---
## 5. Flow Configuration
### 5.1 Update Existing Screen Flow
**Step 1: Open Flow in Flow Builder**
1. Setup → Flows → [Your Form Selection Flow]
2. Click **Edit**
**Step 2: Modify Template Loop Logic**
**Old Logic** (sends one envelope per template):
```
Loop through selected templates
└─ Create envelope for template
```
**New Logic** (sends one envelope with all templates):
```
Get selected templates (checkbox collection)
└─ Apex Action: Send Composite Envelope
Input: templateIds (checkbox collection)
Output: envelopeId
```
**Step 3: Add Apex Action Element**
1. Drag **Action** element onto canvas (after template selection screen)
2. **Action**: `Send Composite Docusign Envelope`
3. **Label**: Send Composite Envelope
4. **API Name**: Send_Composite_Envelope
5. **Set Input Values**:
- `templateIds` = `{!SelectedTemplateIds}` (collection from checkboxes)
- `recordId` = `{!recordId}` (from flow start)
- `language` = `{!SelectedLanguage}` (from language selection)
- `emailSubject` = "Please review and sign these forms"
6. **Store Output Values**:
- `envelopeId``{!EnvelopeId}` (text variable)
- `success``{!Success}` (boolean variable)
- `errorMessage``{!ErrorMessage}` (text variable)
**Step 4: Add Decision Element (Success/Error)**
1. Drag **Decision** element after Apex Action
2. **Outcome 1: Success**
- Condition: `{!Success}` Equals `true`
- Next: Show success screen
3. **Outcome 2: Error**
- Default outcome
- Next: Show error screen (with `{!ErrorMessage}`)
**Step 5: Remove Old Loop**
1. Delete old loop element that sent individual envelopes
2. Delete old Apex actions (if any) for single-template sending
**Step 6: Save and Activate**
1. Click **Save As** → New version
2. Click **Activate**
---
## 6. Testing
### 6.1 Unit Test Verification
**Run all tests**:
```bash
sf apex run test --wait 10 --result-format human --code-coverage --target-org my-sandbox
```
**Expected code coverage**: >75% on all classes
### 6.2 Integration Test (Manual)
**Test Case 1: Send 3 Templates (Happy Path)**
1. Navigate to Salesforce record
2. Launch Screen Flow
3. Select language: English
4. Check 3 templates: Form A, Form B, Form C
5. Click **Send**
6. **Expected**:
- Success message shown
- Envelope ID displayed
- One envelope created in Docusign
- Three documents in envelope
- Recipients receive one email
**Test Case 2: Send 1 Template (Minimum)**
1. Select 1 template only
2. **Expected**: Envelope created with 1 document
**Test Case 3: Send 14 Templates (Maximum)**
1. Select all 14 templates
2. **Expected**: Envelope created with 14 documents
**Test Case 4: Spanish Language**
1. Select language: Spanish
2. Select 2 Spanish templates
3. **Expected**: Envelope created with Spanish documents
**Test Case 5: Error Handling (Invalid Template)**
1. Manually modify Apex to use invalid template ID
2. **Expected**: Error message displayed, no envelope created
### 6.3 Performance Test
**Measure Apex execution time**:
1. Enable Debug Logs (Setup → Debug Logs)
2. Send envelope with 14 templates
3. Review log file
4. **Expected**: Execution time < 10 seconds
---
## 7. Monitoring
### 7.1 Debug Logs
**Enable for API User**:
1. Setup → Debug Logs → New
2. **Traced Entity Type**: User
3. **Traced Entity Name**: [API User]
4. **Log Level**: FINEST (for troubleshooting) or INFO (for production)
5. **Expiration**: 1 day (or desired duration)
**Review Logs**:
1. Setup → Debug Logs
2. Click **View** on recent log
3. Search for:
- `REQUEST_BODY` (Docusign API request JSON)
- `RESPONSE_BODY` (Docusign API response)
- `CalloutException` (errors)
### 7.2 Custom Logging (Optional)
**Create Custom Object for API Logs**:
1. Setup → Object Manager → Create → Custom Object
2. **Object Name**: Docusign API Log
3. **API Name**: Docusign_API_Log__c
4. Add fields:
- **Envelope_Id__c** (Text, 255)
- **Template_Count__c** (Number, 0 decimals)
- **Status__c** (Picklist: Success, Error)
- **Error_Message__c** (Long Text Area, 32,000)
- **Request_Time__c** (Date/Time)
- **Duration_Ms__c** (Number, 0 decimals)
**Log API calls in Apex**:
```apex
Docusign_API_Log__c log = new Docusign_API_Log__c(
Envelope_Id__c = envelopeId,
Template_Count__c = templateIds.size(),
Status__c = 'Success',
Request_Time__c = System.now(),
Duration_Ms__c = durationMs
);
insert log;
```
---
## 8. Rollback Plan
### 8.1 Quick Rollback (Deactivate Flow)
If issues arise in production:
1. Setup → Flows → [Your Flow]
2. **Deactivate** current version
3. **Activate** previous version (before composite envelope changes)
### 8.2 Full Rollback (Remove Apex)
```bash
sf project deploy start --manifest manifest/destructiveChanges.xml --target-org production
```
**destructiveChanges.xml**:
```xml
<?xml version="1.0" encoding="UTF-8"?>
<Package xmlns="http://soap.sforce.com/2006/04/metadata">
<types>
<members>DocusignCompositeEnvelopeBuilder</members>
<members>DocusignAPIService</members>
<members>DocusignCredentials</members>
<members>DocusignCompositeEnvelopeBuilderTest</members>
<members>DocusignAPIServiceTest</members>
<members>DocusignCredentialsTest</members>
<name>ApexClass</name>
</types>
<version>60.0</version>
</Package>
```
---
## 9. Post-Deployment Checklist
- [ ] Named Credential configured and tested
- [ ] Custom Settings populated with Account ID
- [ ] Remote Site Settings added
- [ ] Apex classes deployed (100% coverage)
- [ ] Unit tests pass
- [ ] Screen Flow updated and activated
- [ ] Integration test completed successfully
- [ ] Debug logs reviewed (no errors)
- [ ] User training completed
- [ ] Documentation updated
- [ ] Rollback plan tested
---
## 10. Troubleshooting
### 10.1 Common Issues
**Issue**: `USER_AUTHENTICATION_FAILED` error
**Cause**: Access token expired or invalid
**Solution**:
- Verify Named Credential configuration
- Check Integration Key and User ID in Docusign
- Re-grant consent if needed
**Issue**: `INVALID_REQUEST_PARAMETER` - Template ID not found
**Cause**: Template doesn't exist or is in different account
**Solution**:
- Verify template IDs in Docusign
- Ensure using correct Docusign account (sandbox vs. production)
**Issue**: Governor limit exceeded (Heap size)
**Cause**: Too many templates or large JSON payload
**Solution**:
- Reduce number of templates per envelope
- Optimize JSON construction (avoid unnecessary string concatenation)
**Issue**: Flow shows no templates
**Cause**: Flow query not finding templates
**Solution**:
- Check Flow SOQL query (if using custom object for template list)
- Verify Docusign template naming convention matches Flow filter
---
## 11. Maintenance
### 11.1 Regular Tasks
**Weekly**:
- Review debug logs for errors
- Check Docusign API usage (approaching rate limits?)
**Monthly**:
- Review custom object logs (if implemented)
- Update templates as needed
**Quarterly**:
- Review access token expiry settings
- Update Apex classes if Docusign API changes
### 11.2 Template Updates
When adding a new form template:
1. Create template in Docusign (English + Spanish versions)
2. Update Flow template list (if hardcoded)
3. Test envelope creation with new template
4. Update user documentation
---
## 12. Support
### 12.1 Internal Support
- **Salesforce Admin**: [Contact info]
- **Docusign Admin**: [Contact info]
- **Developer**: Paul Huliganga
### 12.2 External Resources
- **Salesforce Support**: https://help.salesforce.com
- **Docusign Support**: https://support.docusign.com
- **Docusign Developer Center**: https://developers.docusign.com
---
**Document Version**: 1.0
**Last Updated**: February 23, 2026
**Next Review**: After first production deployment

View File

@ -0,0 +1,647 @@
# Design Document
**Project**: Salesforce Composite Envelope Builder
**Version**: 1.0
**Date**: February 23, 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 │ │ │
│ │ │ - 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
- 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='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,
HttpResponse res,
Long durationMs
)
private static void handleAPIError(HttpResponse response)
```
---
### 2.3 DocusignCredentials (Credential Management)
**Purpose**: Retrieve and manage Docusign API credentials securely
**Responsibilities**:
- Fetch credentials from Named Credential or Custom Settings
- Handle token refresh (JWT/OAuth2)
- Provide credential object to service layer
**Methods**:
```apex
public static DocusignCredentials getInstance()
public String getAccessToken()
public String getAccountId()
public String getBaseUrl()
private static String refreshAccessToken() // JWT or OAuth2 refresh
public class CredentialException extends Exception {}
```
**Properties**:
```apex
public String baseUrl { get; private set; }
public String accountId { get; private set; }
public String accessToken { get; private set; }
public DateTime tokenExpiry { get; private set; }
```
---
## 3. Data Flow
### 3.1 Sequence Diagram: Send Composite Envelope
```
User Flow Apex Service Docusign API
│ │ │ │ │
│ Select │ │ │ │
│ Templates │ │ │ │
├────────────▶│ │ │ │
│ │ │ │ │
│ │ Invoke Apex │ │ │
│ │ Action │ │ │
│ ├──────────────▶│ │ │
│ │ (templateIds) │ │ │
│ │ │ │ │
│ │ │ Validate inputs │ │
│ │ ├────────┐ │ │
│ │ │ │ │ │
│ │ │◀───────┘ │ │
│ │ │ │ │
│ │ │ Build JSON │ │
│ │ ├────────┐ │ │
│ │ │ │ │ │
│ │ │◀───────┘ │ │
│ │ │ │ │
│ │ │ Get credentials │ │
│ │ ├───────────────────▶│ │
│ │ │ │ │
│ │ │◀───────────────────┤ │
│ │ │ (creds) │ │
│ │ │ │ │
│ │ │ Call API │ │
│ │ ├───────────────────▶│ │
│ │ │ (JSON, creds) │ │
│ │ │ │ │
│ │ │ │ POST /envelopes │
│ │ │ ├─────────────────▶│
│ │ │ │ │
│ │ │ │ 201 Created │
│ │ │ │ {envelopeId} │
│ │ │ │◀─────────────────┤
│ │ │ │ │
│ │ │ envelopeId │ │
│ │ │◀───────────────────┤ │
│ │ │ │ │
│ │ Result │ │ │
│ │ (envelopeId) │ │ │
│ │◀──────────────┤ │ │
│ │ │ │ │
│ Success │ │ │ │
│ Message │ │ │ │
│◀────────────┤ │ │ │
│ │ │ │ │
```
### 3.2 Error Handling Flow
```
Apex Service Docusign API
│ │ │
│ Call API │ │
├────────────────▶│ │
│ │ POST /envelopes │
│ ├─────────────────▶│
│ │ │
│ │ 400 Bad Request │
│ │ (error JSON) │
│ │◀─────────────────┤
│ │ │
│ │ Parse error │
│ ├──────┐ │
│ │ │ │
│ │◀─────┘ │
│ │ │
│ │ Throw exception │
│ │ │
│ Catch exception │ │
│◀────────────────┤ │
│ │ │
│ Log error │ │
├──────┐ │ │
│ │ │ │
│◀─────┘ │ │
│ │ │
│ Return Result │ │
│ (success=false, │ │
│ errorMessage) │ │
│ │ │
```
---
## 4. Docusign Composite Template JSON Structure
### 4.1 Request Format
```json
{
"status": "sent",
"emailSubject": "Please review and sign these forms",
"compositeTemplates": [
{
"compositeTemplateId": "1",
"serverTemplates": [
{
"sequence": "1",
"templateId": "TEMPLATE_ID_FORM_A"
}
]
},
{
"compositeTemplateId": "2",
"serverTemplates": [
{
"sequence": "2",
"templateId": "TEMPLATE_ID_FORM_B"
}
]
},
{
"compositeTemplateId": "3",
"serverTemplates": [
{
"sequence": "3",
"templateId": "TEMPLATE_ID_FORM_C"
}
]
}
]
}
```
### 4.2 Apex JSON Construction
**Approach**: Use `Map<String, Object>` + `JSON.serialize()` for dynamic construction
```apex
List<Object> compositeTemplates = new List<Object>();
Integer sequence = 1;
for (String templateId : sortedTemplateIds) {
Map<String, Object> template = new Map<String, Object>{
'compositeTemplateId' => String.valueOf(sequence),
'serverTemplates' => new List<Object>{
new Map<String, Object>{
'sequence' => String.valueOf(sequence),
'templateId' => templateId
}
}
};
compositeTemplates.add(template);
sequence++;
}
Map<String, Object> envelope = new Map<String, Object>{
'status' => 'sent',
'emailSubject' => emailSubject,
'compositeTemplates' => compositeTemplates
};
String envelopeJSON = JSON.serialize(envelope);
```
---
## 5. API Integration Details
### 5.1 Endpoint
**POST** `https://{baseUrl}/restapi/v2.1/accounts/{accountId}/envelopes`
Where:
- `{baseUrl}`: e.g., `na3.docusign.net` (from Named Credential)
- `{accountId}`: Docusign account GUID (from Named Credential or Custom Settings)
### 5.2 Headers
```
Authorization: Bearer {access_token}
Content-Type: application/json
Accept: application/json
```
### 5.3 Authentication
**JWT (JSON Web Token) - Preferred**:
- Use RSA key pair stored in Named Credential
- Generate JWT, exchange for access token
- Token valid for 1 hour, cache and refresh as needed
**OAuth2 Authorization Code - Alternative**:
- User-based authentication
- Access token + refresh token
- Requires user interaction for initial authorization
### 5.4 Response Codes
| Code | Meaning | Action |
|------|---------|--------|
| 201 | Created | Success - parse envelope ID |
| 400 | Bad Request | Invalid JSON or parameters - log and return error |
| 401 | Unauthorized | Token expired - refresh and retry |
| 403 | Forbidden | Insufficient permissions - log and return error |
| 429 | Too Many Requests | Rate limit - retry after delay |
| 500 | Server Error | Docusign issue - log and return error |
---
## 6. Governor Limit Considerations
### 6.1 Heap Size
- **Limit**: 6 MB (synchronous), 12 MB (asynchronous)
- **Mitigation**:
- Serialize JSON incrementally
- Avoid loading large objects into memory
- Process template IDs in batches if needed (future enhancement)
### 6.2 CPU Time
- **Limit**: 10,000 ms (synchronous)
- **Mitigation**:
- Minimize loops and complex logic
- Delegate heavy work to Docusign API
### 6.3 Callouts
- **Limit**: 100 callouts per transaction, 10-second timeout per callout
- **Mitigation**:
- Single API call per envelope
- Use asynchronous callouts (@future) for bulk operations (future enhancement)
### 6.4 SOQL Queries
- **Limit**: 100 queries per transaction
- **Mitigation**:
- Minimize queries in loop (not applicable for this design)
- Cache credentials if fetched via SOQL
---
## 7. Security Design
### 7.1 Credential Storage
**Option 1: Named Credential (Preferred)**
- Credentials encrypted by Salesforce
- No code changes needed for credential updates
- OAuth flows managed by platform
**Option 2: Protected Custom Settings**
- API credentials stored encrypted
- Accessible only via Apex
- Manual token refresh required
### 7.2 Access Control
- Apex class runs in **user context** (sharing rules enforced)
- Users must have:
- Permission to execute Flows
- Read access to Salesforce record
- Docusign send permission (managed via Docusign)
### 7.3 Data Protection
- No sensitive data logged in debug statements
- API credentials never exposed in logs or error messages
- HTTPS for all API communication
---
## 8. Testing Strategy
### 8.1 Unit Tests
**Test Coverage Goal**: >75% (Salesforce requirement)
**Test Classes**:
- `DocusignCompositeEnvelopeBuilderTest`
- `DocusignAPIServiceTest`
- `DocusignCredentialsTest`
**Test Scenarios**:
1. **Success case**: 3 templates combined, envelope created
2. **Edge case**: 1 template (minimum)
3. **Edge case**: 14 templates (maximum)
4. **Error case**: Invalid template ID
5. **Error case**: API returns 400
6. **Error case**: API returns 401 (token expired)
7. **Error case**: API timeout
8. **Mock callouts**: Use `HttpCalloutMock` for Docusign API
### 8.2 Integration Tests
**Manual Testing**:
1. Create test Docusign templates in sandbox
2. Configure Named Credential with sandbox credentials
3. Execute Screen Flow with various template combinations
4. Verify envelope creation in Docusign sandbox
5. Verify documents written back to Salesforce
### 8.3 Performance Tests
- Test with maximum templates (14)
- Measure Apex CPU time
- Verify heap size usage
- Test concurrent requests (multiple users)
---
## 9. Deployment Architecture
### 9.1 Deployment Components
**Metadata to Deploy**:
1. Apex classes:
- `DocusignCompositeEnvelopeBuilder.cls`
- `DocusignAPIService.cls`
- `DocusignCredentials.cls`
- Test classes (3 files)
2. Named Credential configuration
3. Remote Site Settings (for Docusign API URL)
4. Permission Sets (optional, for access control)
### 9.2 Deployment Steps
1. **Sandbox deployment** (via VS Code or CLI):
```bash
sf project deploy start --target-org sandbox
```
2. **Run unit tests**:
```bash
sf apex run test --wait 5
```
3. **Validate production** (without deployment):
```bash
sf project deploy validate --target-org production
```
4. **Deploy to production**:
```bash
sf project deploy start --target-org production
```
### 9.3 Rollback Plan
- Version control: Git repository tracks all changes
- Destructive changes: Remove Apex classes if needed
- Flow modification: Update Flow to call old logic temporarily
---
## 10. Monitoring & Maintenance
### 10.1 Logging
**Debug Logs**:
- Log API request/response (sanitized, no credentials)
- Log execution time
- Log errors with stack traces
**Custom Object (Optional)**:
- Store API call history in custom object `Docusign_API_Log__c`
- Fields: Timestamp, Envelope ID, Template Count, Status, Error Message
### 10.2 Alerts
- Email alerts for repeated API failures
- Platform Event for real-time monitoring (future enhancement)
### 10.3 Maintenance Tasks
- Monitor Docusign API rate limits
- Review logs for errors
- Update templates as business needs change
- Refresh API credentials before expiry
---
## 11. Future Enhancements
### 11.1 Phase 2 Features
1. **Dynamic recipient selection**: Override template recipients
2. **Conditional form selection**: Show/hide forms based on Salesforce data
3. **Bulk sending**: Send envelopes to multiple records
4. **Template caching**: Cache template metadata to reduce API calls
### 11.2 Technical Improvements
1. **Asynchronous processing**: Use `@future` or Queueable for large batches
2. **Retry logic**: Automatic retry for transient failures
3. **Rate limit handling**: Intelligent backoff and retry
4. **Analytics dashboard**: Lightning component for usage reporting
---
## 12. Glossary
| Term | Definition |
|------|------------|
| **Composite Template** | Docusign feature to combine multiple templates into one envelope |
| **Invocable Method** | Apex method callable from Screen Flows, Process Builder |
| **Named Credential** | Salesforce feature for secure external credential storage |
| **JWT** | JSON Web Token - authentication method for server-to-server integration |
| **Governor Limits** | Salesforce platform resource limits (CPU, heap, callouts) |
---
**Document Status**: Draft
**Next Steps**: Review with stakeholders, begin Apex development
**Estimated Effort**: 3-5 days development + 2 days testing

View File

@ -0,0 +1,304 @@
# Requirements Document
**Project**: Salesforce Composite Envelope Builder
**Version**: 1.0
**Date**: February 23, 2026
**Author**: Paul Huliganga
---
## 1. Executive Summary
### 1.1 Purpose
Enable Salesforce users to dynamically select and send multiple Docusign form templates in a single envelope, eliminating the need for pre-built combination templates.
### 1.2 Business Problem
- Current system has **42 pre-built templates** for various combinations of 14 forms
- Maintenance burden: every new combination requires a new template
- Cannot handle all possible combinations (14 forms = 16,384 possible combinations)
- User confusion: difficult to find the right pre-built combination template
### 1.3 Solution Overview
Replace combination templates with **28 single-form templates** (14 forms × 2 languages). Build custom Apex code to dynamically combine selected templates into one envelope at runtime using Docusign's Composite Templates API.
---
## 2. Business Requirements
### 2.1 Functional Requirements
#### FR-001: Multi-Language Support
**Priority**: High
**Description**: System must support English and Spanish forms
**Acceptance Criteria**:
- User selects language before form selection
- Only templates in selected language are displayed
- All forms in envelope are in same language
#### FR-002: Dynamic Form Selection
**Priority**: High
**Description**: Users can select any combination of forms to include in envelope
**Acceptance Criteria**:
- Forms displayed in alphabetical order by title
- Checkbox interface for selection (existing Screen Flow UI)
- Minimum 1 form, maximum 14 forms per envelope
- User can select all, none, or any combination
#### FR-003: Single Envelope Generation
**Priority**: High
**Description**: All selected forms must be combined into ONE envelope
**Acceptance Criteria**:
- One envelope ID returned
- One email sent to recipients
- One signing ceremony
- Documents appear in selected order
#### FR-004: Consistent Recipients
**Priority**: High
**Description**: All forms go to the same 2 recipients for review and signature
**Acceptance Criteria**:
- Recipient 1: Review and sign
- Recipient 2: Review and sign
- Same roles across all templates
- Recipients automatically merged by Docusign
#### FR-005: Salesforce Document Storage
**Priority**: High
**Description**: Completed envelope documents written back to initiating Salesforce record
**Acceptance Criteria**:
- Documents attached to correct record
- All forms from envelope stored
- Metadata preserved (envelope ID, completion date, signers)
### 2.2 Non-Functional Requirements
#### NFR-001: Performance
**Priority**: Medium
**Description**: Envelope creation must complete within acceptable timeframe
**Acceptance Criteria**:
- Apex execution time < 10 seconds
- No Salesforce governor limit violations
- REST API call timeout handled gracefully
#### NFR-002: Maintainability
**Priority**: High
**Description**: Code must be easy to maintain and extend
**Acceptance Criteria**:
- Clear variable naming
- Inline comments for complex logic
- Modular design (separation of concerns)
- Unit tests with >75% code coverage
#### NFR-003: Error Handling
**Priority**: High
**Description**: System must handle errors gracefully
**Acceptance Criteria**:
- API failures logged and reported to user
- Partial failures do not corrupt data
- Clear error messages for troubleshooting
- Retry logic for transient failures (optional Phase 2)
#### NFR-004: Security
**Priority**: High
**Description**: Docusign credentials and API calls must be secure
**Acceptance Criteria**:
- Credentials stored in Named Credential or Protected Custom Settings
- No hardcoded API keys
- OAuth2 or JWT authentication
- HTTPS for all API calls
---
## 3. Technical Requirements
### 3.1 Salesforce Platform Requirements
#### TR-001: Apex Version
- API version: 60.0 or higher
- Language: Apex
#### TR-002: Salesforce Edition
- Enterprise Edition or higher (required for API access)
- Screen Flows enabled
#### TR-003: Permissions
- Users must have permission to:
- Execute Flows
- Send Docusign envelopes
- Attach files to Salesforce records
### 3.2 Docusign Requirements
#### TR-004: Docusign Account
- Docusign eSignature account (Production or Sandbox)
- REST API enabled
- Composite Templates feature available
#### TR-005: Templates
- 28 single-form templates created:
- 14 English templates
- 14 Spanish templates
- Each template contains:
- Document with Docusign tabs
- 2 recipient roles defined
- Merge fields for Salesforce data
#### TR-006: API Credentials
- Integration Key (Integrator Key)
- User ID with API access
- RSA key pair (for JWT) OR OAuth2 client credentials
- Account ID (GUID)
### 3.3 Integration Requirements
#### TR-007: REST API Endpoint
- Endpoint: `/accounts/{accountId}/envelopes`
- Method: POST
- Request format: JSON (Composite Templates structure)
#### TR-008: Authentication
- Preferred: JWT (JSON Web Token)
- Alternative: OAuth2 Authorization Code Grant
- Access token refresh handling
---
## 4. User Stories
### US-001: Select Forms in English
**As a** Salesforce user
**I want to** select multiple English forms to send
**So that** I can send all required documents in one envelope
**Acceptance Criteria**:
- Given I am on the form selection screen
- When I select "English" as the language
- Then I see all 14 English form templates in alphabetical order
- And I can check any combination of forms
- And I can send them in one envelope
### US-002: Select Forms in Spanish
**As a** Salesforce user
**I want to** select multiple Spanish forms to send
**So that** I can send all required documents in one envelope in Spanish
**Acceptance Criteria**:
- Given I am on the form selection screen
- When I select "Spanish" as the language
- Then I see all 14 Spanish form templates in alphabetical order
- And I can check any combination of forms
- And I can send them in one envelope
### US-003: Send Selected Forms
**As a** Salesforce user
**I want to** send my selected forms in one envelope
**So that** recipients receive all documents together
**Acceptance Criteria**:
- Given I have selected 3 forms
- When I click "Send"
- Then one envelope is created
- And one email is sent to recipients
- And envelope ID is returned to Salesforce
- And I see a success confirmation
### US-004: View Completed Documents
**As a** Salesforce user
**I want to** see completed documents attached to the Salesforce record
**So that** I have a record of what was signed
**Acceptance Criteria**:
- Given an envelope was sent and completed
- When I view the Salesforce record
- Then I see all signed documents attached
- And I see envelope metadata (completion date, signers)
---
## 5. Constraints
### 5.1 Technical Constraints
- **Salesforce Governor Limits**: Must stay within Apex heap size (6 MB synchronous, 12 MB asynchronous), callout limits (100 per transaction), CPU time
- **Docusign API Rate Limits**: Respect Docusign API rate limits (varies by plan)
- **Template Limit**: Maximum 14 forms per envelope (business rule)
### 5.2 Business Constraints
- **Single Language Per Envelope**: Cannot mix English and Spanish in one envelope
- **Same Recipients**: All forms must go to the same 2 recipients
### 5.3 Timeline Constraints
- **Delivery**: Solution needed soon (target: 2-4 weeks)
---
## 6. Assumptions
1. Salesforce Screen Flow already exists and displays templates
2. Docusign templates are already created with correct tabs and roles
3. Docusign API credentials are available (or can be configured)
4. Existing document write-back logic can be reused
5. Users have appropriate Salesforce and Docusign permissions
---
## 7. Dependencies
### 7.1 Internal Dependencies
- Salesforce Screen Flow (existing)
- Docusign templates (must be created)
- Salesforce record structure (for document attachment)
### 7.2 External Dependencies
- Docusign REST API availability
- Network connectivity for API callouts
---
## 8. Success Criteria
### 8.1 Functional Success
- ✅ Users can select any combination of forms
- ✅ All selected forms sent in ONE envelope
- ✅ Documents written back to Salesforce
- ✅ No pre-built combination templates needed
### 8.2 Technical Success
- ✅ Code coverage >75%
- ✅ No Salesforce governor limit violations
- ✅ Error handling tested
- ✅ API integration validated
### 8.3 User Success
- ✅ Easier form selection (alphabetical list vs. searching for combination)
- ✅ Faster envelope creation
- ✅ Reduced maintenance burden
---
## 9. Out of Scope (Phase 1)
The following are explicitly out of scope for the initial release:
1. **Custom recipient selection** - recipients are fixed per template
2. **Dynamic tab positioning** - tabs defined in templates, not runtime
3. **Form preview** - no preview before sending
4. **Bulk sending** - one envelope at a time
5. **Template versioning** - template updates handled manually
6. **Analytics dashboard** - no reporting on form selection patterns
---
## 10. Future Enhancements (Potential Phase 2)
1. **Dynamic recipient selection**: Allow users to override default recipients
2. **Conditional logic**: Show/hide forms based on Salesforce data
3. **Form preview**: Preview selected forms before sending
4. **Bulk operations**: Send envelopes to multiple records
5. **Template management UI**: Admin UI for template configuration
6. **Analytics**: Track which form combinations are most common
---
**Document Status**: Draft
**Next Review**: After design document completion
**Approvers**: [Customer stakeholders]

View File

@ -0,0 +1,55 @@
const { defineConfig } = require('eslint/config');
const eslintJs = require('@eslint/js');
const jestPlugin = require('eslint-plugin-jest');
const auraConfig = require('@salesforce/eslint-plugin-aura');
const lwcConfig = require('@salesforce/eslint-config-lwc/recommended');
const globals = require('globals');
module.exports = defineConfig([
// Aura configuration
{
files: ['**/aura/**/*.js'],
extends: [
...auraConfig.configs.recommended,
...auraConfig.configs.locker
]
},
// LWC configuration
{
files: ['**/lwc/**/*.js'],
extends: [lwcConfig]
},
// LWC configuration with override for LWC test files
{
files: ['**/lwc/**/*.test.js'],
extends: [lwcConfig],
rules: {
'@lwc/lwc/no-unexpected-wire-adapter-usages': 'off'
},
languageOptions: {
globals: {
...globals.node
}
}
},
// Jest mocks configuration
{
files: ['**/jest-mocks/**/*.js'],
languageOptions: {
sourceType: 'module',
ecmaVersion: 'latest',
globals: {
...globals.node,
...globals.es2021,
...jestPlugin.environments.globals.globals
}
},
plugins: {
eslintJs
},
extends: ['eslintJs/recommended']
}
]);

View File

@ -0,0 +1,345 @@
<?xml version="1.0" encoding="UTF-8"?>
<Flow xmlns="http://soap.sforce.com/2006/04/metadata">
<actionCalls>
<name>Invoke_DS_Envelope</name>
<label>Invoke DS Envelope</label>
<locationX>138</locationX>
<locationY>890</locationY>
<actionName>dfsle__EnvelopeConfigurationBulkRequest</actionName>
<actionType>apex</actionType>
<connector>
<targetReference>Iterating_Envelope_Templates</targetReference>
</connector>
<flowTransactionModel>Automatic</flowTransactionModel>
<inputParameters>
<name>envelopeConfigurationId</name>
<value>
<elementReference>Iterating_Envelope_Templates.Id</elementReference>
</value>
</inputParameters>
<inputParameters>
<name>sourceId</name>
<value>
<elementReference>recordId</elementReference>
</value>
</inputParameters>
<nameSegment>dfsle__EnvelopeConfigurationBulkRequest</nameSegment>
</actionCalls>
<apiVersion>60.0</apiVersion>
<areMetricsLoggedToDataCloud>false</areMetricsLoggedToDataCloud>
<decisions>
<name>Check_Row_Selection</name>
<label>Check Row Selection</label>
<locationX>182</locationX>
<locationY>674</locationY>
<defaultConnector>
<targetReference>Row_not_selected</targetReference>
</defaultConnector>
<defaultConnectorLabel>Default Outcome</defaultConnectorLabel>
<rules>
<name>Is_Row_Selected</name>
<conditionLogic>and</conditionLogic>
<conditions>
<leftValueReference>data.firstSelectedRow.Id</leftValueReference>
<operator>IsNull</operator>
<rightValue>
<booleanValue>false</booleanValue>
</rightValue>
</conditions>
<connector>
<targetReference>Iterating_Envelope_Templates</targetReference>
</connector>
<label>Is Row Selected?</label>
</rules>
</decisions>
<decisions>
<name>Is_Language_Selected</name>
<label>Is Language Selected?</label>
<locationX>380</locationX>
<locationY>242</locationY>
<defaultConnector>
<targetReference>Language_Not_Added_Screen</targetReference>
</defaultConnector>
<defaultConnectorLabel>Default Outcome</defaultConnectorLabel>
<rules>
<name>Language_Selected</name>
<conditionLogic>and</conditionLogic>
<conditions>
<leftValueReference>Get_Records.Docusign_Envelope_Language__c</leftValueReference>
<operator>IsNull</operator>
<rightValue>
<booleanValue>false</booleanValue>
</rightValue>
</conditions>
<connector>
<targetReference>Language_Warning_Screen</targetReference>
</connector>
<label>Language Selected?</label>
</rules>
</decisions>
<environments>Default</environments>
<interviewLabel>DocuSign Envelope Templates {!$Flow.CurrentDateTime}</interviewLabel>
<isAdditionalPermissionRequiredToRun>true</isAdditionalPermissionRequiredToRun>
<label>DocuSign Envelope Templates</label>
<loops>
<name>Iterating_Envelope_Templates</name>
<label>Iterating Envelope Templates</label>
<locationX>50</locationX>
<locationY>782</locationY>
<collectionReference>data.selectedRows</collectionReference>
<iterationOrder>Asc</iterationOrder>
<nextValueConnector>
<targetReference>Invoke_DS_Envelope</targetReference>
</nextValueConnector>
<noMoreValuesConnector>
<targetReference>Success_Screen</targetReference>
</noMoreValuesConnector>
</loops>
<processMetadataValues>
<name>BuilderType</name>
<value>
<stringValue>LightningFlowBuilder</stringValue>
</value>
</processMetadataValues>
<processMetadataValues>
<name>CanvasMode</name>
<value>
<stringValue>AUTO_LAYOUT_CANVAS</stringValue>
</value>
</processMetadataValues>
<processMetadataValues>
<name>OriginBuilderType</name>
<value>
<stringValue>LightningFlowBuilder</stringValue>
</value>
</processMetadataValues>
<processType>Flow</processType>
<recordLookups>
<name>DocuSign_Envelope_Templates</name>
<label>DocuSign Envelope Templates</label>
<locationX>182</locationX>
<locationY>458</locationY>
<assignNullValuesIfNoRecordsFound>false</assignNullValuesIfNoRecordsFound>
<connector>
<targetReference>Envelope_template_records</targetReference>
</connector>
<filterLogic>and</filterLogic>
<filters>
<field>Envelope_Template_Language__c</field>
<operator>EqualTo</operator>
<value>
<elementReference>Get_Records.Docusign_Envelope_Language__c</elementReference>
</value>
</filters>
<getFirstRecordOnly>false</getFirstRecordOnly>
<object>dfsle__EnvelopeConfiguration__c</object>
<queriedFields>Id</queriedFields>
<queriedFields>Name</queriedFields>
<storeOutputAutomatically>true</storeOutputAutomatically>
</recordLookups>
<recordLookups>
<name>Get_Records</name>
<label>Get Records</label>
<locationX>380</locationX>
<locationY>134</locationY>
<assignNullValuesIfNoRecordsFound>false</assignNullValuesIfNoRecordsFound>
<connector>
<targetReference>Is_Language_Selected</targetReference>
</connector>
<filterLogic>and</filterLogic>
<filters>
<field>Id</field>
<operator>EqualTo</operator>
<value>
<elementReference>recordId</elementReference>
</value>
</filters>
<getFirstRecordOnly>true</getFirstRecordOnly>
<object>Client_Case__c</object>
<queriedFields>Id</queriedFields>
<queriedFields>Docusign_Envelope_Language__c</queriedFields>
<storeOutputAutomatically>true</storeOutputAutomatically>
</recordLookups>
<screens>
<name>Envelope_template_records</name>
<label>Envelope template records</label>
<locationX>182</locationX>
<locationY>566</locationY>
<allowBack>true</allowBack>
<allowFinish>true</allowFinish>
<allowPause>false</allowPause>
<backButtonLabel>Back</backButtonLabel>
<connector>
<targetReference>Check_Row_Selection</targetReference>
</connector>
<fields>
<name>data</name>
<dataTypeMappings>
<typeName>T</typeName>
<typeValue>dfsle__EnvelopeConfiguration__c</typeValue>
</dataTypeMappings>
<extensionName>flowruntime:datatable</extensionName>
<fieldType>ComponentInstance</fieldType>
<inputParameters>
<name>label</name>
<value>
<stringValue>Data Table</stringValue>
</value>
</inputParameters>
<inputParameters>
<name>selectionMode</name>
<value>
<stringValue>MULTI_SELECT</stringValue>
</value>
</inputParameters>
<inputParameters>
<name>minRowSelection</name>
<value>
<numberValue>0.0</numberValue>
</value>
</inputParameters>
<inputParameters>
<name>tableData</name>
<value>
<elementReference>DocuSign_Envelope_Templates</elementReference>
</value>
</inputParameters>
<inputParameters>
<name>columns</name>
<value>
<stringValue>[{&quot;apiName&quot;:&quot;Name&quot;,&quot;guid&quot;:&quot;column-6d57&quot;,&quot;editable&quot;:false,&quot;hasCustomHeaderLabel&quot;:true,&quot;customHeaderLabel&quot;:&quot;Envelope Template Name&quot;,&quot;wrapText&quot;:true,&quot;order&quot;:0,&quot;label&quot;:&quot;Name&quot;,&quot;type&quot;:&quot;text&quot;}]</stringValue>
</value>
</inputParameters>
<inputsOnNextNavToAssocScrn>UseStoredValues</inputsOnNextNavToAssocScrn>
<isRequired>true</isRequired>
<storeOutputAutomatically>true</storeOutputAutomatically>
<styleProperties>
<verticalAlignment>
<stringValue>top</stringValue>
</verticalAlignment>
<width>
<stringValue>12</stringValue>
</width>
</styleProperties>
</fields>
<nextOrFinishButtonLabel>Send</nextOrFinishButtonLabel>
<showFooter>true</showFooter>
<showHeader>true</showHeader>
</screens>
<screens>
<name>Language_Not_Added_Screen</name>
<label>Language Not Added Screen</label>
<locationX>578</locationX>
<locationY>350</locationY>
<allowBack>false</allowBack>
<allowFinish>true</allowFinish>
<allowPause>false</allowPause>
<fields>
<name>LanguageNotSelected</name>
<fieldText>&lt;p&gt;The &lt;strong&gt;DocuSign Envelope Language&lt;/strong&gt; is not populated on the record. Please add the language first and then proceed.&lt;/p&gt;</fieldText>
<fieldType>DisplayText</fieldType>
<styleProperties>
<verticalAlignment>
<stringValue>top</stringValue>
</verticalAlignment>
<width>
<stringValue>12</stringValue>
</width>
</styleProperties>
</fields>
<showFooter>true</showFooter>
<showHeader>true</showHeader>
</screens>
<screens>
<name>Language_Warning_Screen</name>
<label>Language Warning Screen</label>
<locationX>182</locationX>
<locationY>350</locationY>
<allowBack>false</allowBack>
<allowFinish>true</allowFinish>
<allowPause>false</allowPause>
<connector>
<targetReference>DocuSign_Envelope_Templates</targetReference>
</connector>
<fields>
<name>LangWarningText</name>
<fieldText>&lt;p&gt;The current selected language is &lt;strong&gt;{!Get_Records.Docusign_Envelope_Language__c}. &lt;/strong&gt;On the next screen you will be able to see form names of {!Get_Records.Docusign_Envelope_Language__c} language only. If you want to switch the language, please go back to record and select another language form &lt;strong&gt;DocuSign Envelope Language&lt;/strong&gt;.&lt;/p&gt;</fieldText>
<fieldType>DisplayText</fieldType>
<styleProperties>
<verticalAlignment>
<stringValue>top</stringValue>
</verticalAlignment>
<width>
<stringValue>12</stringValue>
</width>
</styleProperties>
</fields>
<nextOrFinishButtonLabel>Next</nextOrFinishButtonLabel>
<showFooter>true</showFooter>
<showHeader>true</showHeader>
</screens>
<screens>
<name>Row_not_selected</name>
<label>Row not selected</label>
<locationX>314</locationX>
<locationY>782</locationY>
<allowBack>true</allowBack>
<allowFinish>true</allowFinish>
<allowPause>false</allowPause>
<backButtonLabel>Back</backButtonLabel>
<fields>
<name>ErrorMessage</name>
<fieldText>&lt;p&gt;&lt;strong style=&quot;background-color: rgb(255, 255, 255); color: rgb(68, 68, 68);&quot;&gt;&lt;em&gt;You have not selected any of the forms. Please go back and select the form first and then proceed.&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;</fieldText>
<fieldType>DisplayText</fieldType>
<styleProperties>
<verticalAlignment>
<stringValue>top</stringValue>
</verticalAlignment>
<width>
<stringValue>12</stringValue>
</width>
</styleProperties>
</fields>
<showFooter>true</showFooter>
<showHeader>false</showHeader>
</screens>
<screens>
<name>Success_Screen</name>
<label>Success Screen</label>
<locationX>50</locationX>
<locationY>1082</locationY>
<allowBack>false</allowBack>
<allowFinish>true</allowFinish>
<allowPause>false</allowPause>
<fields>
<name>SuccessMessage</name>
<fieldText>&lt;p&gt;&lt;span style=&quot;font-size: 16px;&quot;&gt;Envelope sent successfully.&lt;/span&gt;&lt;/p&gt;</fieldText>
<fieldType>DisplayText</fieldType>
<styleProperties>
<verticalAlignment>
<stringValue>top</stringValue>
</verticalAlignment>
<width>
<stringValue>12</stringValue>
</width>
</styleProperties>
</fields>
<showFooter>true</showFooter>
<showHeader>false</showHeader>
</screens>
<start>
<locationX>254</locationX>
<locationY>0</locationY>
<connector>
<targetReference>Get_Records</targetReference>
</connector>
</start>
<status>Active</status>
<variables>
<name>recordId</name>
<dataType>String</dataType>
<isCollection>false</isCollection>
<isInput>true</isInput>
<isOutput>false</isOutput>
</variables>
</Flow>

View File

@ -0,0 +1,347 @@
<?xml version="1.0" encoding="UTF-8"?>
<Flow xmlns="http://soap.sforce.com/2006/04/metadata">
<actionCalls>
<name>Invoke_DS_Envelope</name>
<label>Invoke DS Envelope</label>
<locationX>138</locationX>
<locationY>890</locationY>
<actionName>dfsle__EnvelopeConfigurationBulkRequest</actionName>
<actionType>apex</actionType>
<connector>
<targetReference>Iterating_Envelope_Templates</targetReference>
</connector>
<flowTransactionModel>Automatic</flowTransactionModel>
<inputParameters>
<name>envelopeConfigurationId</name>
<value>
<elementReference>Iterating_Envelope_Templates.Id</elementReference>
</value>
</inputParameters>
<inputParameters>
<name>sourceId</name>
<value>
<elementReference>recordId</elementReference>
</value>
</inputParameters>
<nameSegment>dfsle__EnvelopeConfigurationBulkRequest</nameSegment>
<offset>0</offset>
</actionCalls>
<apiVersion>60.0</apiVersion>
<areMetricsLoggedToDataCloud>false</areMetricsLoggedToDataCloud>
<decisions>
<name>Check_Row_Selection</name>
<label>Check Row Selection</label>
<locationX>182</locationX>
<locationY>674</locationY>
<defaultConnector>
<targetReference>Row_not_selected</targetReference>
</defaultConnector>
<defaultConnectorLabel>Default Outcome</defaultConnectorLabel>
<rules>
<name>Is_Row_Selected</name>
<conditionLogic>and</conditionLogic>
<conditions>
<leftValueReference>data.firstSelectedRow.Id</leftValueReference>
<operator>IsNull</operator>
<rightValue>
<booleanValue>false</booleanValue>
</rightValue>
</conditions>
<connector>
<targetReference>Iterating_Envelope_Templates</targetReference>
</connector>
<label>Is Row Selected?</label>
</rules>
</decisions>
<decisions>
<name>Is_Language_Selected</name>
<label>Is Language Selected?</label>
<locationX>380</locationX>
<locationY>242</locationY>
<defaultConnector>
<targetReference>Language_Not_Added_Screen</targetReference>
</defaultConnector>
<defaultConnectorLabel>Default Outcome</defaultConnectorLabel>
<rules>
<name>Language_Selected</name>
<conditionLogic>and</conditionLogic>
<conditions>
<leftValueReference>Get_Records.Docusign_Envelope_Language__c</leftValueReference>
<operator>IsNull</operator>
<rightValue>
<booleanValue>false</booleanValue>
</rightValue>
</conditions>
<connector>
<targetReference>Language_Warning_Screen</targetReference>
</connector>
<label>Language Selected?</label>
</rules>
</decisions>
<environments>Default</environments>
<interviewLabel>Docusign Envelope Templates V2 {!$Flow.CurrentDateTime}</interviewLabel>
<label>Docusign Envelope Templates V2</label>
<loops>
<name>Iterating_Envelope_Templates</name>
<label>Iterating Envelope Templates</label>
<locationX>50</locationX>
<locationY>782</locationY>
<collectionReference>data.selectedRows</collectionReference>
<iterationOrder>Asc</iterationOrder>
<nextValueConnector>
<targetReference>Invoke_DS_Envelope</targetReference>
</nextValueConnector>
<noMoreValuesConnector>
<targetReference>Success_Screen</targetReference>
</noMoreValuesConnector>
</loops>
<processMetadataValues>
<name>BuilderType</name>
<value>
<stringValue>LightningFlowBuilder</stringValue>
</value>
</processMetadataValues>
<processMetadataValues>
<name>CanvasMode</name>
<value>
<stringValue>AUTO_LAYOUT_CANVAS</stringValue>
</value>
</processMetadataValues>
<processMetadataValues>
<name>OriginBuilderType</name>
<value>
<stringValue>LightningFlowBuilder</stringValue>
</value>
</processMetadataValues>
<processType>Flow</processType>
<recordLookups>
<name>DocuSign_Envelope_Templates</name>
<label>DocuSign Envelope Templates</label>
<locationX>182</locationX>
<locationY>458</locationY>
<assignNullValuesIfNoRecordsFound>false</assignNullValuesIfNoRecordsFound>
<connector>
<targetReference>Envelope_template_records</targetReference>
</connector>
<filterLogic>and</filterLogic>
<filters>
<field>Envelope_Template_Language__c</field>
<operator>EqualTo</operator>
<value>
<elementReference>Get_Records.Docusign_Envelope_Language__c</elementReference>
</value>
</filters>
<getFirstRecordOnly>false</getFirstRecordOnly>
<object>dfsle__EnvelopeConfiguration__c</object>
<queriedFields>Id</queriedFields>
<queriedFields>Name</queriedFields>
<sortField>Name</sortField>
<sortOrder>Asc</sortOrder>
<storeOutputAutomatically>true</storeOutputAutomatically>
</recordLookups>
<recordLookups>
<name>Get_Records</name>
<label>Get Records</label>
<locationX>380</locationX>
<locationY>134</locationY>
<assignNullValuesIfNoRecordsFound>false</assignNullValuesIfNoRecordsFound>
<connector>
<targetReference>Is_Language_Selected</targetReference>
</connector>
<filterLogic>and</filterLogic>
<filters>
<field>Id</field>
<operator>EqualTo</operator>
<value>
<elementReference>recordId</elementReference>
</value>
</filters>
<getFirstRecordOnly>true</getFirstRecordOnly>
<object>Client_Case__c</object>
<queriedFields>Id</queriedFields>
<queriedFields>Docusign_Envelope_Language__c</queriedFields>
<storeOutputAutomatically>true</storeOutputAutomatically>
</recordLookups>
<screens>
<name>Envelope_template_records</name>
<label>Envelope template records</label>
<locationX>182</locationX>
<locationY>566</locationY>
<allowBack>true</allowBack>
<allowFinish>true</allowFinish>
<allowPause>false</allowPause>
<backButtonLabel>Back</backButtonLabel>
<connector>
<targetReference>Check_Row_Selection</targetReference>
</connector>
<fields>
<name>data</name>
<dataTypeMappings>
<typeName>T</typeName>
<typeValue>dfsle__EnvelopeConfiguration__c</typeValue>
</dataTypeMappings>
<extensionName>flowruntime:datatable</extensionName>
<fieldType>ComponentInstance</fieldType>
<inputParameters>
<name>label</name>
<value>
<stringValue>Data Table</stringValue>
</value>
</inputParameters>
<inputParameters>
<name>selectionMode</name>
<value>
<stringValue>MULTI_SELECT</stringValue>
</value>
</inputParameters>
<inputParameters>
<name>minRowSelection</name>
<value>
<numberValue>0.0</numberValue>
</value>
</inputParameters>
<inputParameters>
<name>tableData</name>
<value>
<elementReference>DocuSign_Envelope_Templates</elementReference>
</value>
</inputParameters>
<inputParameters>
<name>columns</name>
<value>
<stringValue>[{&quot;apiName&quot;:&quot;Name&quot;,&quot;guid&quot;:&quot;column-6d57&quot;,&quot;editable&quot;:false,&quot;hasCustomHeaderLabel&quot;:true,&quot;customHeaderLabel&quot;:&quot;Envelope Template Name&quot;,&quot;wrapText&quot;:true,&quot;order&quot;:0,&quot;label&quot;:&quot;Name&quot;,&quot;type&quot;:&quot;text&quot;}]</stringValue>
</value>
</inputParameters>
<inputsOnNextNavToAssocScrn>UseStoredValues</inputsOnNextNavToAssocScrn>
<isRequired>true</isRequired>
<storeOutputAutomatically>true</storeOutputAutomatically>
<styleProperties>
<verticalAlignment>
<stringValue>top</stringValue>
</verticalAlignment>
<width>
<stringValue>12</stringValue>
</width>
</styleProperties>
</fields>
<nextOrFinishButtonLabel>Send</nextOrFinishButtonLabel>
<showFooter>true</showFooter>
<showHeader>true</showHeader>
</screens>
<screens>
<name>Language_Not_Added_Screen</name>
<label>Language Not Added Screen</label>
<locationX>578</locationX>
<locationY>350</locationY>
<allowBack>false</allowBack>
<allowFinish>true</allowFinish>
<allowPause>false</allowPause>
<fields>
<name>LanguageNotSelected</name>
<fieldText>&lt;p&gt;The &lt;strong&gt;DocuSign Envelope Language&lt;/strong&gt; is not populated on the record. Please add the language first and then proceed.&lt;/p&gt;</fieldText>
<fieldType>DisplayText</fieldType>
<styleProperties>
<verticalAlignment>
<stringValue>top</stringValue>
</verticalAlignment>
<width>
<stringValue>12</stringValue>
</width>
</styleProperties>
</fields>
<showFooter>true</showFooter>
<showHeader>true</showHeader>
</screens>
<screens>
<name>Language_Warning_Screen</name>
<label>Language Warning Screen</label>
<locationX>182</locationX>
<locationY>350</locationY>
<allowBack>false</allowBack>
<allowFinish>true</allowFinish>
<allowPause>false</allowPause>
<connector>
<targetReference>DocuSign_Envelope_Templates</targetReference>
</connector>
<fields>
<name>LangWarningText</name>
<fieldText>&lt;p&gt;The current selected language is &lt;strong&gt;{!Get_Records.Docusign_Envelope_Language__c}. &lt;/strong&gt;On the next screen you will be able to see form names of {!Get_Records.Docusign_Envelope_Language__c} language only. If you want to switch the language, please go back to record and select another language form &lt;strong&gt;DocuSign Envelope Language&lt;/strong&gt;.&lt;/p&gt;</fieldText>
<fieldType>DisplayText</fieldType>
<styleProperties>
<verticalAlignment>
<stringValue>top</stringValue>
</verticalAlignment>
<width>
<stringValue>12</stringValue>
</width>
</styleProperties>
</fields>
<nextOrFinishButtonLabel>Next</nextOrFinishButtonLabel>
<showFooter>true</showFooter>
<showHeader>true</showHeader>
</screens>
<screens>
<name>Row_not_selected</name>
<label>Row not selected</label>
<locationX>314</locationX>
<locationY>782</locationY>
<allowBack>true</allowBack>
<allowFinish>true</allowFinish>
<allowPause>false</allowPause>
<backButtonLabel>Back</backButtonLabel>
<fields>
<name>ErrorMessage</name>
<fieldText>&lt;p&gt;&lt;strong style=&quot;background-color: rgb(255, 255, 255); color: rgb(68, 68, 68);&quot;&gt;&lt;em&gt;You have not selected any of the forms. Please go back and select the form first and then proceed.&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;</fieldText>
<fieldType>DisplayText</fieldType>
<styleProperties>
<verticalAlignment>
<stringValue>top</stringValue>
</verticalAlignment>
<width>
<stringValue>12</stringValue>
</width>
</styleProperties>
</fields>
<showFooter>true</showFooter>
<showHeader>false</showHeader>
</screens>
<screens>
<name>Success_Screen</name>
<label>Success Screen</label>
<locationX>50</locationX>
<locationY>1082</locationY>
<allowBack>false</allowBack>
<allowFinish>true</allowFinish>
<allowPause>false</allowPause>
<fields>
<name>SuccessMessage</name>
<fieldText>&lt;p&gt;&lt;span style=&quot;font-size: 16px;&quot;&gt;Envelope sent successfully.&lt;/span&gt;&lt;/p&gt;</fieldText>
<fieldType>DisplayText</fieldType>
<styleProperties>
<verticalAlignment>
<stringValue>top</stringValue>
</verticalAlignment>
<width>
<stringValue>12</stringValue>
</width>
</styleProperties>
</fields>
<showFooter>true</showFooter>
<showHeader>false</showHeader>
</screens>
<start>
<locationX>254</locationX>
<locationY>0</locationY>
<connector>
<targetReference>Get_Records</targetReference>
</connector>
</start>
<status>Active</status>
<variables>
<name>recordId</name>
<dataType>String</dataType>
<isCollection>false</isCollection>
<isInput>true</isInput>
<isOutput>false</isOutput>
</variables>
</Flow>

View File

@ -0,0 +1,271 @@
/**
* @description Service class for Docusign REST API interactions
* @author Paul Huliganga
* @date 2026-02-23
*/
public with sharing class DocusignAPIService {
private static final Integer TIMEOUT_MS = 120000; // 120 seconds
private static final Integer MAX_RETRIES = 2;
/**
* @description Creates a composite envelope in Docusign
* @param envelopeJSON JSON string containing envelope definition
* @param creds Docusign credentials object
* @return Envelope ID (GUID)
* @throws CalloutException if API call fails
*/
public static String createCompositeEnvelope(
String envelopeJSON,
DocusignCredentials creds
) {
Long startTime = System.currentTimeMillis();
// Build HTTP request
HttpRequest req = buildCreateEnvelopeRequest(envelopeJSON, creds);
// Make callout with retry logic
HttpResponse res = executeWithRetry(req, MAX_RETRIES);
Long duration = System.currentTimeMillis() - startTime;
// Log request/response (sanitized)
logAPICall(req, res, duration);
// Parse response
if (res.getStatusCode() == 201) {
return parseEnvelopeId(res);
} else {
handleAPIError(res);
return null; // Won't reach here, handleAPIError throws
}
}
/**
* @description Builds HTTP request for creating envelope
* @param envelopeJSON JSON body
* @param creds Credentials
* @return Configured HttpRequest
*/
@TestVisible
private static HttpRequest buildCreateEnvelopeRequest(
String envelopeJSON,
DocusignCredentials creds
) {
HttpRequest req = new HttpRequest();
// Use Named Credential if available, otherwise build URL manually
String endpoint = buildEnvelopeEndpoint(creds);
req.setEndpoint(endpoint);
req.setMethod('POST');
req.setHeader('Authorization', 'Bearer ' + creds.getAccessToken());
req.setHeader('Content-Type', 'application/json');
req.setHeader('Accept', 'application/json');
req.setBody(envelopeJSON);
req.setTimeout(TIMEOUT_MS);
return req;
}
/**
* @description Builds envelope endpoint URL
* @param creds Docusign credentials
* @return Full endpoint URL
*/
private static String buildEnvelopeEndpoint(DocusignCredentials creds) {
// Check if using Named Credential (starts with 'callout:')
if (creds.getBaseUrl().startsWith('callout:')) {
return creds.getBaseUrl() + '/accounts/' + creds.getAccountId() + '/envelopes';
} else {
return creds.getBaseUrl() + '/restapi/v2.1/accounts/' + creds.getAccountId() + '/envelopes';
}
}
/**
* @description Executes HTTP request with retry logic
* @param req HTTP request
* @param maxRetries Maximum number of retry attempts
* @return HTTP response
* @throws CalloutException if all retries fail
*/
@TestVisible
private static HttpResponse executeWithRetry(HttpRequest req, Integer maxRetries) {
Http http = new Http();
HttpResponse res;
Integer attempt = 0;
while (attempt <= maxRetries) {
attempt++;
try {
res = http.send(req);
// Success or non-retryable error
if (res.getStatusCode() == 201 || !isRetryableError(res.getStatusCode())) {
return res;
}
// Rate limit - wait before retry
if (res.getStatusCode() == 429 && attempt <= maxRetries) {
Integer waitMs = calculateBackoff(attempt);
System.debug('Rate limited, waiting ' + waitMs + 'ms before retry ' + attempt);
// Note: Apex doesn't have Thread.sleep, but in production this would be handled by Platform Events
// For now, just retry immediately
}
} catch (System.CalloutException e) {
// Timeout or connection error
if (attempt > maxRetries) {
throw new CalloutException('Docusign API callout failed after ' + maxRetries + ' retries: ' + e.getMessage());
}
System.debug('Callout failed (attempt ' + attempt + '): ' + e.getMessage());
}
}
// All retries exhausted
throw new CalloutException('Docusign API callout failed after ' + maxRetries + ' retries. Last status: ' + res.getStatusCode());
}
/**
* @description Determines if an HTTP status code is retryable
* @param statusCode HTTP status code
* @return True if error is transient and should be retried
*/
private static Boolean isRetryableError(Integer statusCode) {
// 401 (token expired), 429 (rate limit), 500-599 (server errors)
return statusCode == 401 || statusCode == 429 || (statusCode >= 500 && statusCode < 600);
}
/**
* @description Calculates exponential backoff delay
* @param attempt Retry attempt number
* @return Delay in milliseconds
*/
private static Integer calculateBackoff(Integer attempt) {
// Exponential backoff: 2^attempt * 1000ms (1s, 2s, 4s, ...)
return (Integer) Math.pow(2, attempt) * 1000;
}
/**
* @description Parses envelope ID from successful response
* @param res HTTP response
* @return Envelope ID (GUID)
*/
@TestVisible
private static String parseEnvelopeId(HttpResponse res) {
try {
Map<String, Object> responseMap = (Map<String, Object>) JSON.deserializeUntyped(res.getBody());
String envelopeId = (String) responseMap.get('envelopeId');
if (String.isBlank(envelopeId)) {
throw new CalloutException('Envelope ID not found in Docusign response');
}
return envelopeId;
} catch (Exception e) {
throw new CalloutException('Failed to parse Docusign response: ' + e.getMessage());
}
}
/**
* @description Handles API error responses
* @param res HTTP response with error
* @throws CalloutException with detailed error message
*/
@TestVisible
private static void handleAPIError(HttpResponse res) {
String errorMessage = 'Docusign API error [' + res.getStatusCode() + ']';
try {
Map<String, Object> errorBody = (Map<String, Object>) JSON.deserializeUntyped(res.getBody());
String errorCode = (String) errorBody.get('errorCode');
String message = (String) errorBody.get('message');
if (String.isNotBlank(errorCode)) {
errorMessage += ' ' + errorCode;
}
if (String.isNotBlank(message)) {
errorMessage += ': ' + message;
}
} catch (Exception e) {
// Could not parse error body, use raw response
errorMessage += ': ' + res.getBody();
}
// Map common errors to user-friendly messages
errorMessage = enhanceErrorMessage(res.getStatusCode(), errorMessage);
throw new CalloutException(errorMessage);
}
/**
* @description Enhances error messages with user-friendly guidance
* @param statusCode HTTP status code
* @param originalMessage Original error message
* @return Enhanced error message
*/
private static String enhanceErrorMessage(Integer statusCode, String originalMessage) {
switch on statusCode {
when 400 {
return originalMessage + ' - Check template IDs and request parameters.';
}
when 401 {
return originalMessage + ' - Authentication failed. Check API credentials and access token.';
}
when 403 {
return originalMessage + ' - User lacks permission to create envelopes.';
}
when 404 {
return originalMessage + ' - One or more templates not found in Docusign account.';
}
when 429 {
return originalMessage + ' - API rate limit exceeded. Please try again later.';
}
when 500, 503 {
return originalMessage + ' - Docusign server error. Please try again.';
}
when else {
return originalMessage;
}
}
}
/**
* @description Logs API call details (sanitized, no credentials)
* @param req HTTP request
* @param res HTTP response
* @param durationMs Duration in milliseconds
*/
private static void logAPICall(HttpRequest req, HttpResponse res, Long durationMs) {
System.debug(LoggingLevel.INFO, '=== Docusign API Call ===');
System.debug(LoggingLevel.INFO, 'Method: ' + req.getMethod());
System.debug(LoggingLevel.INFO, 'Endpoint: ' + sanitizeEndpoint(req.getEndpoint()));
System.debug(LoggingLevel.INFO, 'Request Body Length: ' + req.getBody().length() + ' bytes');
System.debug(LoggingLevel.INFO, 'Response Status: ' + res.getStatusCode() + ' ' + res.getStatus());
System.debug(LoggingLevel.INFO, 'Response Body Length: ' + res.getBody().length() + ' bytes');
System.debug(LoggingLevel.INFO, 'Duration: ' + durationMs + 'ms');
// Only log full bodies in DEBUG mode (not in production)
if (Test.isRunningTest()) {
System.debug(LoggingLevel.FINEST, 'Request Body: ' + req.getBody());
System.debug(LoggingLevel.FINEST, 'Response Body: ' + res.getBody());
}
}
/**
* @description Sanitizes endpoint URL for logging (removes account ID)
* @param endpoint Full endpoint URL
* @return Sanitized URL
*/
private static String sanitizeEndpoint(String endpoint) {
// Replace account ID with placeholder for security
return endpoint.replaceAll('/accounts/[a-f0-9\\-]+/', '/accounts/{accountId}/');
}
/**
* @description Custom exception for API errors
*/
public class CalloutException extends Exception {}
}

View File

@ -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>

View File

@ -0,0 +1,323 @@
/**
* @description Test class for DocusignAPIService
* @author Paul Huliganga
* @date 2026-02-23
*/
@isTest
private class DocusignAPIServiceTest {
/**
* @description Mock HTTP callout
*/
private class DocusignMock implements HttpCalloutMock {
private Integer statusCode;
private String responseBody;
private Integer callCount = 0;
public DocusignMock(Integer statusCode, String responseBody) {
this.statusCode = statusCode;
this.responseBody = responseBody;
}
public HTTPResponse respond(HTTPRequest req) {
callCount++;
HttpResponse res = new HttpResponse();
res.setStatusCode(this.statusCode);
res.setBody(this.responseBody);
res.setStatus(this.statusCode == 201 ? 'Created' : 'Error');
return res;
}
}
/**
* @description Mock that fails first time, succeeds second (for retry testing)
*/
private class RetryMock implements HttpCalloutMock {
private Integer callCount = 0;
public HTTPResponse respond(HTTPRequest req) {
callCount++;
HttpResponse res = new HttpResponse();
if (callCount == 1) {
// First call fails with 500
res.setStatusCode(500);
res.setBody('{"errorCode":"INTERNAL_ERROR","message":"Server error"}');
res.setStatus('Internal Server Error');
} else {
// Second call succeeds
res.setStatusCode(201);
res.setBody('{"envelopeId":"envelope-retry-success","status":"sent"}');
res.setStatus('Created');
}
return res;
}
}
/**
* @description Test successful envelope creation
*/
@isTest
static void testSuccessfulEnvelopeCreation() {
// Arrange
String envelopeId = 'envelope-test-12345';
String mockResponse = '{"envelopeId":"' + envelopeId + '","status":"sent"}';
Test.setMock(HttpCalloutMock.class, new DocusignMock(201, mockResponse));
DocusignCredentials.setTestCredentials('test-account-id', 'callout:DocusignAPI', 'test-token');
DocusignCredentials creds = DocusignCredentials.getInstance();
String envelopeJSON = '{"status":"sent","compositeTemplates":[]}';
// Act
Test.startTest();
String resultEnvelopeId = DocusignAPIService.createCompositeEnvelope(envelopeJSON, creds);
Test.stopTest();
// Assert
System.assertEquals(envelopeId, resultEnvelopeId, 'Should return envelope ID');
}
/**
* @description Test parseEnvelopeId method
*/
@isTest
static void testParseEnvelopeId() {
// Arrange
HttpResponse res = new HttpResponse();
res.setStatusCode(201);
res.setBody('{"envelopeId":"parsed-envelope-123","uri":"/envelopes/parsed-envelope-123"}');
// Act
Test.startTest();
String envelopeId = DocusignAPIService.parseEnvelopeId(res);
Test.stopTest();
// Assert
System.assertEquals('parsed-envelope-123', envelopeId, 'Should parse envelope ID correctly');
}
/**
* @description Test parseEnvelopeId with missing ID
*/
@isTest
static void testParseEnvelopeIdMissing() {
// Arrange
HttpResponse res = new HttpResponse();
res.setStatusCode(201);
res.setBody('{"status":"sent"}'); // No envelopeId field
// Act & Assert
Test.startTest();
try {
DocusignAPIService.parseEnvelopeId(res);
System.assert(false, 'Should have thrown exception');
} catch (DocusignAPIService.CalloutException e) {
System.assert(e.getMessage().contains('Envelope ID not found'), 'Should mention missing ID');
}
Test.stopTest();
}
/**
* @description Test handleAPIError for 400 Bad Request
*/
@isTest
static void testHandleAPIError400() {
// Arrange
HttpResponse res = new HttpResponse();
res.setStatusCode(400);
res.setBody('{"errorCode":"INVALID_REQUEST_PARAMETER","message":"Invalid template ID"}');
// Act & Assert
Test.startTest();
try {
DocusignAPIService.handleAPIError(res);
System.assert(false, 'Should have thrown exception');
} catch (DocusignAPIService.CalloutException e) {
System.assert(e.getMessage().contains('400'), 'Should include status code');
System.assert(e.getMessage().contains('Invalid template'), 'Should include error message');
System.assert(e.getMessage().contains('template IDs'), 'Should include guidance');
}
Test.stopTest();
}
/**
* @description Test handleAPIError for 401 Unauthorized
*/
@isTest
static void testHandleAPIError401() {
// Arrange
HttpResponse res = new HttpResponse();
res.setStatusCode(401);
res.setBody('{"errorCode":"USER_AUTHENTICATION_FAILED","message":"Invalid access token"}');
// Act & Assert
Test.startTest();
try {
DocusignAPIService.handleAPIError(res);
System.assert(false, 'Should have thrown exception');
} catch (DocusignAPIService.CalloutException e) {
System.assert(e.getMessage().contains('401'), 'Should include status code');
System.assert(e.getMessage().contains('Authentication'), 'Should mention authentication');
}
Test.stopTest();
}
/**
* @description Test handleAPIError for 403 Forbidden
*/
@isTest
static void testHandleAPIError403() {
// Arrange
HttpResponse res = new HttpResponse();
res.setStatusCode(403);
res.setBody('{"errorCode":"USER_LACKS_PERMISSIONS","message":"User cannot send envelopes"}');
// Act & Assert
Test.startTest();
try {
DocusignAPIService.handleAPIError(res);
System.assert(false, 'Should have thrown exception');
} catch (DocusignAPIService.CalloutException e) {
System.assert(e.getMessage().contains('403'), 'Should include status code');
System.assert(e.getMessage().contains('permission'), 'Should mention permission');
}
Test.stopTest();
}
/**
* @description Test handleAPIError for 404 Not Found
*/
@isTest
static void testHandleAPIError404() {
// Arrange
HttpResponse res = new HttpResponse();
res.setStatusCode(404);
res.setBody('{"errorCode":"RESOURCE_NOT_FOUND","message":"Template not found"}');
// Act & Assert
Test.startTest();
try {
DocusignAPIService.handleAPIError(res);
System.assert(false, 'Should have thrown exception');
} catch (DocusignAPIService.CalloutException e) {
System.assert(e.getMessage().contains('404'), 'Should include status code');
System.assert(e.getMessage().contains('not found'), 'Should mention template not found');
}
Test.stopTest();
}
/**
* @description Test handleAPIError for 429 Rate Limit
*/
@isTest
static void testHandleAPIError429() {
// Arrange
HttpResponse res = new HttpResponse();
res.setStatusCode(429);
res.setBody('{"errorCode":"HOURLY_APIINVOCATION_LIMIT_EXCEEDED","message":"Rate limit exceeded"}');
// Act & Assert
Test.startTest();
try {
DocusignAPIService.handleAPIError(res);
System.assert(false, 'Should have thrown exception');
} catch (DocusignAPIService.CalloutException e) {
System.assert(e.getMessage().contains('429'), 'Should include status code');
System.assert(e.getMessage().contains('rate limit'), 'Should mention rate limit');
}
Test.stopTest();
}
/**
* @description Test handleAPIError for 500 Server Error
*/
@isTest
static void testHandleAPIError500() {
// Arrange
HttpResponse res = new HttpResponse();
res.setStatusCode(500);
res.setBody('{"errorCode":"INTERNAL_ERROR","message":"Server error"}');
// Act & Assert
Test.startTest();
try {
DocusignAPIService.handleAPIError(res);
System.assert(false, 'Should have thrown exception');
} catch (DocusignAPIService.CalloutException e) {
System.assert(e.getMessage().contains('500'), 'Should include status code');
System.assert(e.getMessage().contains('server error'), 'Should mention server error');
}
Test.stopTest();
}
/**
* @description Test retry logic with transient failure
*/
@isTest
static void testRetryLogic() {
// Arrange
Test.setMock(HttpCalloutMock.class, new RetryMock());
DocusignCredentials.setTestCredentials('test-account-id', 'callout:DocusignAPI', 'test-token');
DocusignCredentials creds = DocusignCredentials.getInstance();
String envelopeJSON = '{"status":"sent","compositeTemplates":[]}';
// Act
Test.startTest();
String envelopeId = DocusignAPIService.createCompositeEnvelope(envelopeJSON, creds);
Test.stopTest();
// Assert
System.assertEquals('envelope-retry-success', envelopeId, 'Should succeed after retry');
}
/**
* @description Test buildCreateEnvelopeRequest
*/
@isTest
static void testBuildCreateEnvelopeRequest() {
// Arrange
DocusignCredentials.setTestCredentials('test-account-id', 'callout:DocusignAPI', 'test-token');
DocusignCredentials creds = DocusignCredentials.getInstance();
String envelopeJSON = '{"status":"sent"}';
// Act
Test.startTest();
HttpRequest req = DocusignAPIService.buildCreateEnvelopeRequest(envelopeJSON, creds);
Test.stopTest();
// Assert
System.assertEquals('POST', req.getMethod(), 'Should use POST method');
System.assert(req.getEndpoint().contains('/envelopes'), 'Should have envelopes endpoint');
System.assert(req.getHeader('Authorization').contains('Bearer'), 'Should have Bearer token');
System.assertEquals('application/json', req.getHeader('Content-Type'), 'Should set JSON content type');
System.assertEquals(envelopeJSON, req.getBody(), 'Should set body');
}
/**
* @description Test executeWithRetry with non-retryable error
*/
@isTest
static void testExecuteWithRetryNonRetryable() {
// Arrange
String mockResponse = '{"errorCode":"INVALID_REQUEST_PARAMETER","message":"Bad request"}';
Test.setMock(HttpCalloutMock.class, new DocusignMock(400, mockResponse));
DocusignCredentials.setTestCredentials('test-account-id', 'callout:DocusignAPI', 'test-token');
DocusignCredentials creds = DocusignCredentials.getInstance();
HttpRequest req = DocusignAPIService.buildCreateEnvelopeRequest('{}', creds);
// Act
Test.startTest();
HttpResponse res = DocusignAPIService.executeWithRetry(req, 2);
Test.stopTest();
// Assert
System.assertEquals(400, res.getStatusCode(), 'Should return 400 without retry');
}
}

View File

@ -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>

View File

@ -0,0 +1,327 @@
/**
* @description Main invocable class for combining multiple Docusign templates into a single envelope
* @author Paul Huliganga
* @date 2026-02-23
*/
global with sharing class DocusignCompositeEnvelopeBuilder {
/**
* @description Invocable method called from Salesforce Screen Flow
* @param requests List of request objects containing template IDs and metadata
* @return List of result objects with envelope ID and status
*/
@InvocableMethod(
label='Send Composite Docusign Envelope'
description='Combines multiple Docusign templates into a single envelope'
category='Docusign'
)
public static List<Result> sendCompositeEnvelope(List<Request> requests) {
List<Result> results = new List<Result>();
// Process first request (Flow only sends one)
if (requests == null || requests.isEmpty()) {
return buildErrorResult('No request provided');
}
Request req = requests[0];
Result result = new Result();
try {
// Validate inputs
validateInputs(req);
// Remove duplicates and sort alphabetically
List<String> sortedTemplateIds = sortTemplatesAlphabetically(
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
DocusignCredentials creds = DocusignCredentials.getInstance();
// Call Docusign API
String envelopeId = DocusignAPIService.createCompositeEnvelope(
envelopeJSON,
creds
);
// Success
result.envelopeId = envelopeId;
result.success = true;
result.errorMessage = null;
// Log success
logAPICall(req.templateIds.size(), envelopeId, 'Success', null);
} catch (Exception e) {
// Error handling
result.success = false;
result.errorMessage = e.getMessage();
result.envelopeId = null;
// Log error
logAPICall(
req.templateIds != null ? req.templateIds.size() : 0,
null,
'Error',
e.getMessage() + '\n' + e.getStackTraceString()
);
// Re-throw if critical (governor limits)
if (e instanceof System.LimitException) {
throw e;
}
}
results.add(result);
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)
* @param templateCount Number of templates in envelope
* @param envelopeId Docusign envelope ID
* @param status Success or Error
* @param errorMessage Error message if applicable
*/
private static void logAPICall(
Integer templateCount,
String envelopeId,
String status,
String errorMessage
) {
System.debug(LoggingLevel.INFO, '=== Docusign Composite Envelope API Call ===');
System.debug(LoggingLevel.INFO, 'Timestamp: ' + System.now());
System.debug(LoggingLevel.INFO, 'Template Count: ' + templateCount);
System.debug(LoggingLevel.INFO, 'Envelope ID: ' + envelopeId);
System.debug(LoggingLevel.INFO, 'Status: ' + status);
if (String.isNotBlank(errorMessage)) {
System.debug(LoggingLevel.ERROR, 'Error: ' + errorMessage);
}
// Future enhancement: Insert into Docusign_API_Log__c custom object
}
/**
* @description Helper to build error result
* @param errorMessage Error message
* @return List containing single error result
*/
private static List<Result> buildErrorResult(String errorMessage) {
Result result = new Result();
result.success = false;
result.errorMessage = errorMessage;
result.envelopeId = null;
return new List<Result>{ result };
}
/**
* @description Input parameters for invocable method (from Screen Flow)
*/
global class Request {
@InvocableVariable(
label='Template IDs'
description='List of Docusign template IDs to combine'
required=true
)
public List<String> templateIds;
@InvocableVariable(
label='Salesforce Record ID'
description='ID of the Salesforce record to attach documents to'
required=true
)
public String recordId;
@InvocableVariable(
label='Language'
description='Language code (en or es)'
required=false
)
public String language;
@InvocableVariable(
label='Email Subject'
description='Subject line for envelope email'
required=false
)
public String emailSubject;
}
/**
* @description Output parameters for invocable method (to Screen Flow)
*/
global class Result {
@InvocableVariable(
label='Envelope ID'
description='Docusign envelope ID'
)
public String envelopeId;
@InvocableVariable(
label='Success'
description='True if envelope was created successfully'
)
public Boolean success;
@InvocableVariable(
label='Error Message'
description='Error message if envelope creation failed'
)
public String errorMessage;
}
}

View File

@ -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>

View File

@ -0,0 +1,312 @@
/**
* @description Test class for DocusignCompositeEnvelopeBuilder
* @author Paul Huliganga
* @date 2026-02-23
*/
@isTest
private class DocusignCompositeEnvelopeBuilderTest {
/**
* @description Mock HTTP callout for Docusign API
*/
private class DocusignMock implements HttpCalloutMock {
private Integer statusCode;
private String responseBody;
public DocusignMock(Integer statusCode, String responseBody) {
this.statusCode = statusCode;
this.responseBody = responseBody;
}
public HTTPResponse respond(HTTPRequest req) {
HttpResponse res = new HttpResponse();
res.setStatusCode(this.statusCode);
res.setBody(this.responseBody);
res.setStatus(this.statusCode == 201 ? 'Created' : 'Error');
return res;
}
}
/**
* @description Setup test data
*/
@testSetup
static void setup() {
// Create Custom Setting for credentials
Docusign_Configuration__c config = new Docusign_Configuration__c();
config.Account_Id__c = 'test-account-id-12345';
config.Base_URL__c = 'callout:DocusignAPI';
insert config;
}
/**
* @description Test successful envelope creation with 3 templates
*/
@isTest
static void testSuccessfulEnvelopeCreation() {
// Arrange
String envelopeId = 'envelope-12345-abcde';
String mockResponse = '{"envelopeId":"' + envelopeId + '","status":"sent"}';
Test.setMock(HttpCalloutMock.class, new DocusignMock(201, mockResponse));
DocusignCredentials.setTestCredentials('test-account-id', 'callout:DocusignAPI', 'test-token');
DocusignCompositeEnvelopeBuilder.Request req = new DocusignCompositeEnvelopeBuilder.Request();
req.templateIds = new List<String>{'template-1', 'template-2', 'template-3'};
req.recordId = '001000000ABC123';
req.language = 'en';
req.emailSubject = 'Test Envelope';
// Act
Test.startTest();
List<DocusignCompositeEnvelopeBuilder.Result> results =
DocusignCompositeEnvelopeBuilder.sendCompositeEnvelope(new List<DocusignCompositeEnvelopeBuilder.Request>{req});
Test.stopTest();
// Assert
System.assertEquals(1, results.size(), 'Should return 1 result');
System.assertEquals(true, results[0].success, 'Should be successful');
System.assertEquals(envelopeId, results[0].envelopeId, 'Should return envelope ID');
System.assertEquals(null, results[0].errorMessage, 'Should have no error message');
}
/**
* @description Test with single template (minimum)
*/
@isTest
static void testSingleTemplate() {
// Arrange
String mockResponse = '{"envelopeId":"envelope-single","status":"sent"}';
Test.setMock(HttpCalloutMock.class, new DocusignMock(201, mockResponse));
DocusignCredentials.setTestCredentials('test-account-id', 'callout:DocusignAPI', 'test-token');
DocusignCompositeEnvelopeBuilder.Request req = new DocusignCompositeEnvelopeBuilder.Request();
req.templateIds = new List<String>{'template-1'};
req.recordId = '001000000ABC123';
// Act
Test.startTest();
List<DocusignCompositeEnvelopeBuilder.Result> results =
DocusignCompositeEnvelopeBuilder.sendCompositeEnvelope(new List<DocusignCompositeEnvelopeBuilder.Request>{req});
Test.stopTest();
// Assert
System.assertEquals(true, results[0].success, 'Should be successful with 1 template');
}
/**
* @description Test with 14 templates (maximum)
*/
@isTest
static void testMaximumTemplates() {
// Arrange
String mockResponse = '{"envelopeId":"envelope-max","status":"sent"}';
Test.setMock(HttpCalloutMock.class, new DocusignMock(201, mockResponse));
DocusignCredentials.setTestCredentials('test-account-id', 'callout:DocusignAPI', 'test-token');
List<String> templateIds = new List<String>();
for (Integer i = 1; i <= 14; i++) {
templateIds.add('template-' + i);
}
DocusignCompositeEnvelopeBuilder.Request req = new DocusignCompositeEnvelopeBuilder.Request();
req.templateIds = templateIds;
req.recordId = '001000000ABC123';
// Act
Test.startTest();
List<DocusignCompositeEnvelopeBuilder.Result> results =
DocusignCompositeEnvelopeBuilder.sendCompositeEnvelope(new List<DocusignCompositeEnvelopeBuilder.Request>{req});
Test.stopTest();
// Assert
System.assertEquals(true, results[0].success, 'Should be successful with 14 templates');
}
/**
* @description Test with duplicate template IDs (should be deduplicated)
*/
@isTest
static void testDuplicateTemplates() {
// Arrange
String mockResponse = '{"envelopeId":"envelope-dedup","status":"sent"}';
Test.setMock(HttpCalloutMock.class, new DocusignMock(201, mockResponse));
DocusignCredentials.setTestCredentials('test-account-id', 'callout:DocusignAPI', 'test-token');
DocusignCompositeEnvelopeBuilder.Request req = new DocusignCompositeEnvelopeBuilder.Request();
req.templateIds = new List<String>{'template-1', 'template-2', 'template-1'}; // Duplicate
req.recordId = '001000000ABC123';
// Act
Test.startTest();
List<DocusignCompositeEnvelopeBuilder.Result> results =
DocusignCompositeEnvelopeBuilder.sendCompositeEnvelope(new List<DocusignCompositeEnvelopeBuilder.Request>{req});
Test.stopTest();
// Assert
System.assertEquals(true, results[0].success, 'Should handle duplicates');
}
// Custom fields test removed - not supported in InvocableVariable (Phase 2 enhancement)
/**
* @description Test validation failure - no template IDs
*/
@isTest
static void testValidationNoTemplates() {
// Arrange
DocusignCredentials.setTestCredentials('test-account-id', 'callout:DocusignAPI', 'test-token');
DocusignCompositeEnvelopeBuilder.Request req = new DocusignCompositeEnvelopeBuilder.Request();
req.templateIds = new List<String>(); // Empty
req.recordId = '001000000ABC123';
// Act
Test.startTest();
List<DocusignCompositeEnvelopeBuilder.Result> results =
DocusignCompositeEnvelopeBuilder.sendCompositeEnvelope(new List<DocusignCompositeEnvelopeBuilder.Request>{req});
Test.stopTest();
// Assert
System.assertEquals(false, results[0].success, 'Should fail validation');
System.assert(results[0].errorMessage.containsIgnoreCase('template'), 'Error should mention templates');
}
/**
* @description Test validation failure - too many templates
*/
@isTest
static void testValidationTooManyTemplates() {
// Arrange
DocusignCredentials.setTestCredentials('test-account-id', 'callout:DocusignAPI', 'test-token');
List<String> templateIds = new List<String>();
for (Integer i = 1; i <= 15; i++) {
templateIds.add('template-' + i);
}
DocusignCompositeEnvelopeBuilder.Request req = new DocusignCompositeEnvelopeBuilder.Request();
req.templateIds = templateIds; // 15 templates (> max of 14)
req.recordId = '001000000ABC123';
// Act
Test.startTest();
List<DocusignCompositeEnvelopeBuilder.Result> results =
DocusignCompositeEnvelopeBuilder.sendCompositeEnvelope(new List<DocusignCompositeEnvelopeBuilder.Request>{req});
Test.stopTest();
// Assert
System.assertEquals(false, results[0].success, 'Should fail validation');
System.assert(results[0].errorMessage.contains('Maximum 14'), 'Error should mention limit');
}
/**
* @description Test validation failure - no record ID
*/
@isTest
static void testValidationNoRecordId() {
// Arrange
DocusignCredentials.setTestCredentials('test-account-id', 'callout:DocusignAPI', 'test-token');
DocusignCompositeEnvelopeBuilder.Request req = new DocusignCompositeEnvelopeBuilder.Request();
req.templateIds = new List<String>{'template-1'};
req.recordId = ''; // Blank
// Act
Test.startTest();
List<DocusignCompositeEnvelopeBuilder.Result> results =
DocusignCompositeEnvelopeBuilder.sendCompositeEnvelope(new List<DocusignCompositeEnvelopeBuilder.Request>{req});
Test.stopTest();
// Assert
System.assertEquals(false, results[0].success, 'Should fail validation');
System.assert(results[0].errorMessage.contains('record ID'), 'Error should mention record ID');
}
/**
* @description Test API error - 400 Bad Request
*/
@isTest
static void testAPIError400() {
// Arrange
String mockResponse = '{"errorCode":"INVALID_REQUEST_PARAMETER","message":"Invalid template ID"}';
Test.setMock(HttpCalloutMock.class, new DocusignMock(400, mockResponse));
DocusignCredentials.setTestCredentials('test-account-id', 'callout:DocusignAPI', 'test-token');
DocusignCompositeEnvelopeBuilder.Request req = new DocusignCompositeEnvelopeBuilder.Request();
req.templateIds = new List<String>{'invalid-template'};
req.recordId = '001000000ABC123';
// Act
Test.startTest();
List<DocusignCompositeEnvelopeBuilder.Result> results =
DocusignCompositeEnvelopeBuilder.sendCompositeEnvelope(new List<DocusignCompositeEnvelopeBuilder.Request>{req});
Test.stopTest();
// Assert
System.assertEquals(false, results[0].success, 'Should fail with API error');
System.assert(results[0].errorMessage.contains('400'), 'Error should include status code');
}
/**
* @description Test API error - 401 Unauthorized
*/
@isTest
static void testAPIError401() {
// Arrange
String mockResponse = '{"errorCode":"USER_AUTHENTICATION_FAILED","message":"Invalid token"}';
Test.setMock(HttpCalloutMock.class, new DocusignMock(401, mockResponse));
DocusignCredentials.setTestCredentials('test-account-id', 'callout:DocusignAPI', 'invalid-token');
DocusignCompositeEnvelopeBuilder.Request req = new DocusignCompositeEnvelopeBuilder.Request();
req.templateIds = new List<String>{'template-1'};
req.recordId = '001000000ABC123';
// Act
Test.startTest();
List<DocusignCompositeEnvelopeBuilder.Result> results =
DocusignCompositeEnvelopeBuilder.sendCompositeEnvelope(new List<DocusignCompositeEnvelopeBuilder.Request>{req});
Test.stopTest();
// Assert
System.assertEquals(false, results[0].success, 'Should fail with auth error');
System.assertNotEquals(null, results[0].errorMessage, 'Should have error message');
System.assert(String.isNotBlank(results[0].errorMessage), 'Error message should not be blank');
}
/**
* @description Test JSON builder with all parameters
*/
@isTest
static void testJSONBuilder() {
// Arrange
List<String> templateIds = new List<String>{'template-A', 'template-B'};
String recordId = '001000000ABC123';
String language = 'es';
String emailSubject = 'Custom Subject';
// Act
Test.startTest();
String json = DocusignCompositeEnvelopeBuilder.buildCompositeEnvelopeJSON(
templateIds,
recordId,
language,
emailSubject,
null // customFields - not supported in Phase 1
);
Test.stopTest();
// Assert
System.assertNotEquals(null, json, 'JSON should be generated');
System.assert(json.contains('template-A'), 'Should contain first template');
System.assert(json.contains('template-B'), 'Should contain second template');
System.assert(json.contains(emailSubject), 'Should contain email subject');
System.assert(json.contains('SalesforceRecordId'), 'Should contain record ID field');
System.assert(json.contains(language), 'Should contain language');
}
}

View File

@ -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>

View File

@ -0,0 +1,187 @@
/**
* @description Manages Docusign API credentials and access tokens
* @author Paul Huliganga
* @date 2026-02-23
*/
public with sharing class DocusignCredentials {
private String baseUrl;
private String accountId;
private String accessToken;
private DateTime tokenExpiry;
// Singleton instance
private static DocusignCredentials instance;
// Flag to skip loadCredentials for setTestCredentials
private static Boolean skipLoadForNextInstance = false;
/**
* @description Private constructor (singleton pattern)
*/
private DocusignCredentials() {
// Only load credentials if not explicitly skipped (for setTestCredentials)
if (!skipLoadForNextInstance) {
loadCredentials();
}
skipLoadForNextInstance = false; // Reset flag
}
/**
* @description Gets singleton instance of credentials
* @return DocusignCredentials instance
*/
public static DocusignCredentials getInstance() {
if (instance == null) {
instance = new DocusignCredentials();
}
// Refresh token if expired
if (instance.isTokenExpired()) {
instance.refreshAccessToken();
}
return instance;
}
/**
* @description Loads credentials from Named Credential or Custom Settings
*/
private void loadCredentials() {
// Option 1: Using Named Credential (preferred)
// When using Named Credential, the platform handles authentication automatically
// We just need the account ID and base URL
try {
// Try to load from Custom Settings first
Docusign_Configuration__c config = Docusign_Configuration__c.getOrgDefaults();
if (config != null && String.isNotBlank(config.Account_Id__c)) {
this.accountId = config.Account_Id__c;
this.baseUrl = String.isNotBlank(config.Base_URL__c)
? config.Base_URL__c
: 'callout:DocusignAPI'; // Default to Named Credential
// If using Named Credential, token is managed by platform
if (this.baseUrl.startsWith('callout:')) {
this.accessToken = 'MANAGED_BY_NAMED_CREDENTIAL';
this.tokenExpiry = DateTime.now().addHours(1);
} else {
// Manual token management would go here
// For now, throw exception - must configure Named Credential
throw new CredentialException('Manual token management not implemented. Please use Named Credential.');
}
} else {
throw new CredentialException('Docusign credentials not configured. Please set up Custom Settings: Docusign_Configuration__c');
}
} catch (Exception e) {
throw new CredentialException('Failed to load Docusign credentials: ' + e.getMessage());
}
}
/**
* @description Checks if access token is expired
* @return True if token is expired or about to expire (within 5 minutes)
*/
private Boolean isTokenExpired() {
if (this.tokenExpiry == null) {
return true;
}
// Refresh 5 minutes before expiry to avoid edge cases
DateTime threshold = DateTime.now().addMinutes(5);
return this.tokenExpiry < threshold;
}
/**
* @description Refreshes access token (JWT or OAuth2)
* Note: In production with Named Credential, this is handled automatically by Salesforce
*/
private void refreshAccessToken() {
// If using Named Credential, no manual refresh needed
if (this.baseUrl.startsWith('callout:')) {
this.tokenExpiry = DateTime.now().addHours(1);
return;
}
// Manual JWT token refresh would go here
// This is a placeholder for future enhancement
throw new CredentialException('Manual token refresh not implemented. Use Named Credential for automatic token management.');
}
/**
* @description Gets access token
* @return Access token string
*/
public String getAccessToken() {
if (isTokenExpired()) {
refreshAccessToken();
}
return this.accessToken;
}
/**
* @description Gets Docusign account ID
* @return Account ID (GUID)
*/
public String getAccountId() {
return this.accountId;
}
/**
* @description Gets base URL for Docusign API
* @return Base URL (either Named Credential callout or full URL)
*/
public String getBaseUrl() {
return this.baseUrl;
}
/**
* @description Validates that credentials are properly configured
* @return True if valid
* @throws CredentialException if invalid
*/
public Boolean validate() {
if (String.isBlank(this.accountId)) {
throw new CredentialException('Docusign Account ID is not configured');
}
if (String.isBlank(this.baseUrl)) {
throw new CredentialException('Docusign Base URL is not configured');
}
if (String.isBlank(this.accessToken)) {
throw new CredentialException('Docusign Access Token is not available');
}
return true;
}
/**
* @description Resets singleton instance (for testing)
*/
@TestVisible
private static void resetInstance() {
instance = null;
}
/**
* @description Sets credentials manually (for testing)
*/
@TestVisible
private static void setTestCredentials(String testAccountId, String testBaseUrl, String testAccessToken) {
// Set flag to skip loadCredentials for this instance
skipLoadForNextInstance = true;
instance = new DocusignCredentials();
// Override with specific test values
instance.accountId = testAccountId;
instance.baseUrl = testBaseUrl;
instance.accessToken = testAccessToken;
instance.tokenExpiry = DateTime.now().addHours(1);
}
/**
* @description Custom exception for credential errors
*/
public class CredentialException extends Exception {}
}

View File

@ -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>

View File

@ -0,0 +1,258 @@
/**
* @description Test class for DocusignCredentials
* @author Paul Huliganga
* @date 2026-02-23
*/
@isTest
private class DocusignCredentialsTest {
/**
* @description Setup test data
*/
@testSetup
static void setup() {
// Create Custom Setting for credentials
Docusign_Configuration__c config = new Docusign_Configuration__c();
config.Account_Id__c = 'test-account-id-12345';
config.Base_URL__c = 'callout:DocusignAPI';
insert config;
}
/**
* @description Test singleton pattern
*/
@isTest
static void testGetInstance() {
// Act
Test.startTest();
DocusignCredentials creds1 = DocusignCredentials.getInstance();
DocusignCredentials creds2 = DocusignCredentials.getInstance();
Test.stopTest();
// Assert
System.assertEquals(creds1, creds2, 'Should return same instance (singleton)');
}
/**
* @description Test loading credentials from Custom Settings
*/
@isTest
static void testLoadCredentialsFromCustomSettings() {
// Reset instance to force fresh load from Custom Settings
DocusignCredentials.resetInstance();
// Act
Test.startTest();
DocusignCredentials creds = DocusignCredentials.getInstance();
Test.stopTest();
// Assert
System.assertEquals('test-account-id-12345', creds.getAccountId(), 'Should load account ID');
System.assertEquals('callout:DocusignAPI', creds.getBaseUrl(), 'Should load base URL');
System.assertNotEquals(null, creds.getAccessToken(), 'Should have access token');
}
/**
* @description Test getAccessToken method
*/
@isTest
static void testGetAccessToken() {
// Arrange
DocusignCredentials.resetInstance();
// Act
Test.startTest();
DocusignCredentials creds = DocusignCredentials.getInstance();
String token = creds.getAccessToken();
Test.stopTest();
// Assert
System.assertNotEquals(null, token, 'Should return access token');
}
/**
* @description Test getAccountId method
*/
@isTest
static void testGetAccountId() {
// Reset instance to force fresh load from Custom Settings
DocusignCredentials.resetInstance();
// Act
Test.startTest();
DocusignCredentials creds = DocusignCredentials.getInstance();
String accountId = creds.getAccountId();
Test.stopTest();
// Assert
System.assertEquals('test-account-id-12345', accountId, 'Should return account ID');
}
/**
* @description Test getBaseUrl method
*/
@isTest
static void testGetBaseUrl() {
// Act
Test.startTest();
DocusignCredentials creds = DocusignCredentials.getInstance();
String baseUrl = creds.getBaseUrl();
Test.stopTest();
// Assert
System.assertEquals('callout:DocusignAPI', baseUrl, 'Should return base URL');
}
/**
* @description Test validate method with valid credentials
*/
@isTest
static void testValidateSuccess() {
// Act
Test.startTest();
DocusignCredentials creds = DocusignCredentials.getInstance();
Boolean isValid = creds.validate();
Test.stopTest();
// Assert
System.assertEquals(true, isValid, 'Should validate successfully');
}
/**
* @description Test validate method with missing account ID
*/
@isTest
static void testValidateMissingAccountId() {
// Arrange
DocusignCredentials.resetInstance();
DocusignCredentials.setTestCredentials('', 'callout:DocusignAPI', 'test-token');
// Act & Assert
Test.startTest();
try {
DocusignCredentials creds = DocusignCredentials.getInstance();
creds.validate();
System.assert(false, 'Should have thrown exception');
} catch (DocusignCredentials.CredentialException e) {
System.assert(e.getMessage().contains('Account ID'), 'Should mention Account ID');
}
Test.stopTest();
}
/**
* @description Test validate method with missing base URL
*/
@isTest
static void testValidateMissingBaseUrl() {
// Arrange
DocusignCredentials.resetInstance();
DocusignCredentials.setTestCredentials('test-account-id', '', 'test-token');
// Act & Assert
Test.startTest();
try {
DocusignCredentials creds = DocusignCredentials.getInstance();
creds.validate();
System.assert(false, 'Should have thrown exception');
} catch (DocusignCredentials.CredentialException e) {
System.assert(e.getMessage().contains('Base URL'), 'Should mention Base URL');
}
Test.stopTest();
}
/**
* @description Test validate method with missing access token
*/
@isTest
static void testValidateMissingAccessToken() {
// Arrange
DocusignCredentials.resetInstance();
DocusignCredentials.setTestCredentials('test-account-id', 'callout:DocusignAPI', '');
// Act & Assert
Test.startTest();
try {
DocusignCredentials creds = DocusignCredentials.getInstance();
creds.validate();
System.assert(false, 'Should have thrown exception');
} catch (DocusignCredentials.CredentialException e) {
System.assert(e.getMessage().contains('Access Token'), 'Should mention Access Token');
}
Test.stopTest();
}
/**
* @description Test setTestCredentials method
*/
@isTest
static void testSetTestCredentials() {
// Arrange
DocusignCredentials.resetInstance();
// Act
Test.startTest();
DocusignCredentials.setTestCredentials('test-acc-123', 'https://test.docusign.net', 'test-token-xyz');
DocusignCredentials creds = DocusignCredentials.getInstance();
Test.stopTest();
// Assert
System.assertEquals('test-acc-123', creds.getAccountId(), 'Should set test account ID');
System.assertEquals('https://test.docusign.net', creds.getBaseUrl(), 'Should set test base URL');
System.assertEquals('test-token-xyz', creds.getAccessToken(), 'Should set test access token');
}
/**
* @description Test resetInstance method
*/
@isTest
static void testResetInstance() {
// Arrange
DocusignCredentials creds1 = DocusignCredentials.getInstance();
// Act
Test.startTest();
DocusignCredentials.resetInstance();
DocusignCredentials creds2 = DocusignCredentials.getInstance();
Test.stopTest();
// Assert
System.assertNotEquals(creds1, creds2, 'Should create new instance after reset');
}
/**
* @description Test error when Custom Settings not configured
*/
@isTest
static void testMissingCustomSettings() {
// Arrange
// Reset instance first
DocusignCredentials.resetInstance();
// Delete the test setup data
delete [SELECT Id FROM Docusign_Configuration__c];
// Act & Assert
Test.startTest();
try {
DocusignCredentials creds = DocusignCredentials.getInstance();
System.assert(false, 'Should have thrown exception');
} catch (DocusignCredentials.CredentialException e) {
System.assert(e.getMessage().containsIgnoreCase('not configured'), 'Should mention not configured');
}
Test.stopTest();
}
/**
* @description Test Named Credential URL format
*/
@isTest
static void testNamedCredentialFormat() {
// Act
Test.startTest();
DocusignCredentials creds = DocusignCredentials.getInstance();
String baseUrl = creds.getBaseUrl();
Test.stopTest();
// Assert
System.assert(baseUrl.startsWith('callout:'), 'Named Credential URL should start with callout:');
}
}

View File

@ -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>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<CustomObject xmlns="http://soap.sforce.com/2006/04/metadata">
<customSettingsType>Hierarchy</customSettingsType>
<description>Configuration for Docusign API integration</description>
<enableFeeds>false</enableFeeds>
<label>Docusign Configuration</label>
<visibility>Public</visibility>
</CustomObject>

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>Account_Id__c</fullName>
<description>Docusign Account ID (GUID)</description>
<externalId>false</externalId>
<label>Account Id</label>
<length>255</length>
<required>false</required>
<trackTrending>false</trackTrending>
<type>Text</type>
<unique>false</unique>
</CustomField>

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>Base_URL__c</fullName>
<description>Docusign API Base URL (e.g., callout:DocusignAPI or https://na3.docusign.net/restapi/v2.1)</description>
<externalId>false</externalId>
<label>Base URL</label>
<length>255</length>
<required>false</required>
<trackTrending>false</trackTrending>
<type>Text</type>
<unique>false</unique>
</CustomField>

View File

@ -0,0 +1,6 @@
const { jestConfig } = require('@salesforce/sfdx-lwc-jest/config');
module.exports = {
...jestConfig,
modulePathIgnorePatterns: ['<rootDir>/.localdevserver']
};

View File

@ -0,0 +1,44 @@
{
"name": "salesforce-app",
"private": true,
"version": "1.0.0",
"description": "Salesforce App",
"scripts": {
"lint": "eslint **/{aura,lwc}/**/*.js",
"test": "npm run test:unit",
"test:unit": "sfdx-lwc-jest",
"test:unit:watch": "sfdx-lwc-jest --watch",
"test:unit:debug": "sfdx-lwc-jest --debug",
"test:unit:coverage": "sfdx-lwc-jest --coverage",
"prettier": "prettier --write \"**/*.{cls,cmp,component,css,html,js,json,md,page,trigger,xml,yaml,yml}\"",
"prettier:verify": "prettier --check \"**/*.{cls,cmp,component,css,html,js,json,md,page,trigger,xml,yaml,yml}\"",
"prepare": "husky || true",
"precommit": "lint-staged"
},
"devDependencies": {
"@lwc/eslint-plugin-lwc": "^3.1.0",
"@prettier/plugin-xml": "^3.4.1",
"@salesforce/eslint-config-lwc": "^4.0.0",
"@salesforce/eslint-plugin-aura": "^3.0.0",
"@salesforce/eslint-plugin-lightning": "^2.0.0",
"@salesforce/sfdx-lwc-jest": "^7.0.2",
"eslint": "^9.29.0",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-jest": "^28.14.0",
"husky": "^9.1.7",
"lint-staged": "^16.1.2",
"prettier": "^3.5.3",
"prettier-plugin-apex": "^2.2.6"
},
"lint-staged": {
"**/*.{cls,cmp,component,css,html,js,json,md,page,trigger,xml,yaml,yml}": [
"prettier --write"
],
"**/{aura,lwc}/**/*.js": [
"eslint"
],
"**/lwc/**": [
"sfdx-lwc-jest -- --bail --findRelatedTests --passWithNoTests"
]
}
}

View File

@ -0,0 +1,10 @@
// Use .apex files to store anonymous Apex.
// You can execute anonymous Apex in VS Code by selecting the
// apex text and running the command:
// SFDX: Execute Anonymous Apex with Currently Selected Text
// You can also execute the entire file by running the command:
// SFDX: Execute Anonymous Apex with Editor Contents
string tempvar = 'Enter_your_name_here';
System.debug('Hello World!');
System.debug('My name is ' + tempvar);

View File

@ -0,0 +1,6 @@
// Use .soql files to store SOQL queries.
// You can execute queries in VS Code by selecting the
// query text and running the command:
// SFDX: Execute SOQL Query with Currently Selected Text
SELECT Id, Name FROM Account

View File

@ -0,0 +1,12 @@
{
"packageDirectories": [
{
"path": "force-app",
"default": true
}
],
"name": "composite-envelope-builder",
"namespace": "",
"sfdcLoginUrl": "https://login.salesforce.com",
"sourceApiVersion": "65.0"
}