Initial commit: Salesforce Composite Envelope Builder
This commit is contained in:
commit
4f734f0d17
|
|
@ -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
|
||||||
|
|
@ -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__/**
|
||||||
|
|
@ -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/
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
npm run precommit
|
||||||
|
|
@ -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/
|
||||||
|
|
@ -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" }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -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! 🚀
|
||||||
|
|
@ -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
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"orgName": "paulh company",
|
||||||
|
"edition": "Developer",
|
||||||
|
"features": ["EnableSetPasswordInApi"],
|
||||||
|
"settings": {
|
||||||
|
"lightningExperienceSettings": {
|
||||||
|
"enableS1DesktopEnabled": true
|
||||||
|
},
|
||||||
|
"mobileSettings": {
|
||||||
|
"enableS1EncryptedStoragePref2": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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."
|
||||||
|
|
@ -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."
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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]
|
||||||
|
|
@ -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']
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
@ -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>[{"apiName":"Name","guid":"column-6d57","editable":false,"hasCustomHeaderLabel":true,"customHeaderLabel":"Envelope Template Name","wrapText":true,"order":0,"label":"Name","type":"text"}]</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><p>The <strong>DocuSign Envelope Language</strong> is not populated on the record. Please add the language first and then proceed.</p></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><p>The current selected language is <strong>{!Get_Records.Docusign_Envelope_Language__c}. </strong>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 <strong>DocuSign Envelope Language</strong>.</p></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><p><strong style="background-color: rgb(255, 255, 255); color: rgb(68, 68, 68);"><em>You have not selected any of the forms. Please go back and select the form first and then proceed.</em></strong></p></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><p><span style="font-size: 16px;">Envelope sent successfully.</span></p></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>
|
||||||
|
|
@ -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>[{"apiName":"Name","guid":"column-6d57","editable":false,"hasCustomHeaderLabel":true,"customHeaderLabel":"Envelope Template Name","wrapText":true,"order":0,"label":"Name","type":"text"}]</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><p>The <strong>DocuSign Envelope Language</strong> is not populated on the record. Please add the language first and then proceed.</p></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><p>The current selected language is <strong>{!Get_Records.Docusign_Envelope_Language__c}. </strong>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 <strong>DocuSign Envelope Language</strong>.</p></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><p><strong style="background-color: rgb(255, 255, 255); color: rgb(68, 68, 68);"><em>You have not selected any of the forms. Please go back and select the form first and then proceed.</em></strong></p></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><p><span style="font-size: 16px;">Envelope sent successfully.</span></p></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>
|
||||||
|
|
@ -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 {}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
|
||||||
|
<apiVersion>61.0</apiVersion>
|
||||||
|
<status>Active</status>
|
||||||
|
</ApexClass>
|
||||||
|
|
@ -0,0 +1,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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
|
||||||
|
<apiVersion>61.0</apiVersion>
|
||||||
|
<status>Active</status>
|
||||||
|
</ApexClass>
|
||||||
|
|
@ -0,0 +1,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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
|
||||||
|
<apiVersion>61.0</apiVersion>
|
||||||
|
<status>Active</status>
|
||||||
|
</ApexClass>
|
||||||
|
|
@ -0,0 +1,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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
|
||||||
|
<apiVersion>61.0</apiVersion>
|
||||||
|
<status>Active</status>
|
||||||
|
</ApexClass>
|
||||||
|
|
@ -0,0 +1,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 {}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
|
||||||
|
<apiVersion>61.0</apiVersion>
|
||||||
|
<status>Active</status>
|
||||||
|
</ApexClass>
|
||||||
|
|
@ -0,0 +1,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:');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
|
||||||
|
<apiVersion>61.0</apiVersion>
|
||||||
|
<status>Active</status>
|
||||||
|
</ApexClass>
|
||||||
|
|
@ -0,0 +1,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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
const { jestConfig } = require('@salesforce/sfdx-lwc-jest/config');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
...jestConfig,
|
||||||
|
modulePathIgnorePatterns: ['<rootDir>/.localdevserver']
|
||||||
|
};
|
||||||
|
|
@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
|
@ -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
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"packageDirectories": [
|
||||||
|
{
|
||||||
|
"path": "force-app",
|
||||||
|
"default": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"name": "composite-envelope-builder",
|
||||||
|
"namespace": "",
|
||||||
|
"sfdcLoginUrl": "https://login.salesforce.com",
|
||||||
|
"sourceApiVersion": "65.0"
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue