14 KiB
Project Specification — Phase 2: eSignature Envelope Creation
1. Project Overview
What are we building?
Connecting the CLM document generation workflow to DocuSign eSignature. After a review letter is generated and attached to an Appraiser_Case__c record, the operator can click "Send for Signature" to create a DocuSign envelope (using a pre-built eSignature template), send it, and have the envelope ID, status, and sent timestamp written back to the case.
Why does it matter?
Phase 1 produces the review letter. Phase 2 delivers it for signature. Without this, the operator must manually log into DocuSign to send the envelope — a workflow gap.
Success criteria
- Five eSignature tracking fields exist on
Appraiser_Case__cand deploy cleanly DocusignESignatureService.createEnvelope()calls the eSignature REST API and returns a structured result- Envelope ID, status, and sent timestamp are persisted to the case after a successful send
clmDocGenWorkbenchexposes a "Send for Signature" button that activates after attach succeeds- All Apex changes are covered by tests in the existing test classes; all tests pass in appraiser-dev
2. Technical Foundation
Tech stack
- Language: Apex (Salesforce), LWC (JavaScript/HTML)
- Framework: Salesforce DX / SFDX source format
- Test framework: Apex Test (deployed and run via sf CLI)
- Package manager: N/A — Salesforce metadata deployment
- Org alias:
appraiser-dev
Project structure
force-app/main/default/
├── classes/
│ ├── CLMAdminService.cls
│ ├── CLMAdminServiceTest.cls
│ ├── DocusignESignatureService.cls ← primary change target
│ └── DocusignESignatureServiceTest.cls ← test target
├── lwc/
│ └── clmDocGenWorkbench/ ← UI change target
└── objects/
└── Appraiser_Case__c/
└── fields/ ← 5 new fields
Build and test commands
NODE=/home/paulh/.nvm/versions/node/v22.22.0/bin/node
SF=/home/paulh/.nvm/versions/node/v22.22.0/lib/node_modules/@salesforce/cli/bin/run.js
# Deploy
$NODE $SF project deploy start --source-dir force-app --target-org appraiser-dev
# Run tests
$NODE $SF apex run test \
--target-org appraiser-dev \
--class-names DocusignESignatureServiceTest,CLMAdminServiceTest \
--result-format human \
--synchronous
Coding standards
- Follow existing Apex class structure — inner DTO classes,
@AuraEnabledmethods,with sharing - Follow CLM tracking pattern: service layer method → result object → persist to case in
CLMAdminService - All HTTP callouts mocked in tests using
HttpCalloutMock(seeCLMDocGenCalloutTestfor the pattern) - LWC: follow existing
clmDocGenWorkbenchpatterns (wire adapters, async/await handlers, tracked properties) - Field API names use
__csuffix; field-meta.xml follows Salesforce DX format
3. Requirements
FR-001: eSignature Tracking Fields on Appraiser_Case__c
Description: Five custom fields to track the envelope lifecycle on the case record.
Fields:
ESignature_Envelope_Id__c— Text(255), label "eSignature Envelope ID"ESignature_Envelope_Status__c— Text(50), label "eSignature Envelope Status" (values: created / sent / delivered / completed / voided)ESignature_Sent_At__c— DateTime, label "eSignature Sent At"ESignature_Completed_At__c— DateTime, label "eSignature Completed At"ESignature_Envelope_Url__c— URL(255), label "eSignature Envelope URL"
Acceptance criteria:
- All 5 field metadata XML files exist under
objects/Appraiser_Case__c/fields/ - Fields deploy cleanly to appraiser-dev with no errors
package.xmlis updated to include the new fields (or already coversCustomFieldmembers)
FR-002: createEnvelope() in DocusignESignatureService
Description: An Apex method that creates a DocuSign eSignature envelope using Option A (template-based). The caller provides a case ID, account code, and template ID. The method resolves the named credential from the account setting, calls POST /v2.1/accounts/{accountId}/envelopes with the template ID and a signer role populated from the case's appraiser fields, and returns a structured result.
API call shape:
POST /v2.1/accounts/{accountId}/envelopes
{
"templateId": "<templateId>",
"templateRoles": [
{
"email": "<Appraiser_Email__c>",
"name": "<Appraiser_Name__c or Appraiser_Salutation__c + Appraiser_Last_Name__c>",
"roleName": "Signer"
}
],
"status": "sent"
}
Result class: EnvelopeCreateResult — inner class on DocusignESignatureService:
Boolean successString envelopeIdString statusString envelopeUriString errorMessage
Method signature:
@AuraEnabled(cacheable=false)
public static EnvelopeCreateResult createEnvelope(
Id caseId,
String accountCode,
String templateId
)
Acceptance criteria:
- Method is
@AuraEnabledand callable from LWC - Resolves
eSignatureAccountIdandeSignatureRestNamedCredentialfromCLM_Account_Setting__mdtviaaccountCode - Queries
Appraiser_Email__c,Appraiser_Name__c,Appraiser_Last_Name__c,Appraiser_Salutation__cfrom the case - POSTs correct JSON body to the eSignature API
- Returns
EnvelopeCreateResultwithenvelopeIdandstatuson success - Returns
EnvelopeCreateResultwithsuccess=falseanderrorMessageon failure (non-2xx or exception) - Does NOT throw uncaught exceptions — catches and wraps in result
FR-003: persistEnvelopeResult() in CLMAdminService
Description: After a successful createEnvelope() call, write the result back to Appraiser_Case__c. Follows the same pattern as persistDocGenResult.
Method signature:
@AuraEnabled(cacheable=false)
public static void persistEnvelopeResult(
Id caseId,
String envelopeId,
String envelopeStatus,
String envelopeUri
)
Fields written:
ESignature_Envelope_Id__c= envelopeIdESignature_Envelope_Status__c= envelopeStatusESignature_Sent_At__c= Datetime.now()ESignature_Envelope_Url__c= envelopeUri (if not blank)
Acceptance criteria:
- Method updates the correct case record (by
caseId) - Sets
ESignature_Sent_At__cto current datetime - Does not overwrite
ESignature_Completed_At__c(only set when status = completed)
FR-004: Extend CaseContext with Envelope Fields
Description: The CaseContext inner class in CLMAdminService (and its getCaseContext() query) must include the 5 eSignature tracking fields so the LWC can display current envelope status.
Fields to add to CaseContext:
String eSignatureEnvelopeIdString eSignatureEnvelopeStatusString eSignatureEnvelopeUrlDatetime eSignatureSentAtDatetime eSignatureCompletedAt
Acceptance criteria:
getCaseContext()SOQL query includes all 5 fieldsCaseContextDTO maps all 5 fields with@AuraEnabled
FR-005: "Send for Signature" Button in clmDocGenWorkbench
Description: After the "Attach Generated Document" step succeeds, a "Send for Signature" button becomes enabled. Clicking it calls createEnvelope() and then persistEnvelopeResult(), and displays the result (envelope ID and status, or error message) in the workbench UI.
Behavior:
- Button is disabled until
attachedFileContentDocumentIdis populated (i.e., attach has succeeded) - Button shows a spinner while in progress (same pattern as generate/attach buttons)
- On success: display envelope ID and status; update envelope fields displayed from
caseContext - On error: display error message
- Button does NOT automatically select a template — the operator must provide
templateIdvia a text input field in the UI
New UI elements:
- Text input: "eSignature Template ID" (maps to
templateIdparameter) - Button: "Send for Signature"
- Status display: envelope ID, status, sent timestamp (read from refreshed
caseContext)
Acceptance criteria:
- Button is disabled when
attachedFileContentDocumentIdis blank - Button calls
DocusignESignatureService.createEnvelope()with the case ID, account code, and template ID from the input - On success, calls
CLMAdminService.persistEnvelopeResult()and refreshes the case context display - On failure, displays the error message from
EnvelopeCreateResult.errorMessage - Template ID input is cleared after a successful send
- Spinner shown during the callout; button disabled during in-flight request
NFR-001: Test Coverage
DocusignESignatureServiceTestcoverscreateEnvelope()— at minimum: success path (mock HTTP 201), failure path (mock HTTP 400), missing template ID guardCLMAdminServiceTestcoverspersistEnvelopeResult()— at minimum: fields written correctly,ESignature_Sent_At__cis set- All existing tests continue to pass after each change
NFR-002: No Hardcoded Credentials
- All DocuSign API calls go through named credentials (never hardcoded tokens or passwords)
- Named credential API name is sourced from
CLM_Account_Setting__mdt
4. Data Model
New fields on Appraiser_Case__c
| API Name | Type | Length |
|---|---|---|
| ESignature_Envelope_Id__c | Text | 255 |
| ESignature_Envelope_Status__c | Text | 50 |
| ESignature_Sent_At__c | DateTime | — |
| ESignature_Completed_At__c | DateTime | — |
| ESignature_Envelope_Url__c | URL | 255 |
Existing fields read by createEnvelope()
| API Name | Used for |
|---|---|
| Appraiser_Email__c | templateRoles[0].email |
| Appraiser_Name__c | templateRoles[0].name (prefer this) |
| Appraiser_Salutation__c | fallback for name |
| Appraiser_Last_Name__c | fallback for name |
5. Architecture Decisions
Constraints
- MUST: Use
CLM_Account_Setting__mdtto resolve named credentials — never hardcode - MUST: Follow the
persistDocGenResultpattern inCLMAdminServiceforpersistEnvelopeResult - MUST: Mock all HTTP callouts in tests using
HttpCalloutMock - MUST NOT: Modify the CLM document generation flow (Phase 1) — leave it untouched
- MUST NOT: Create new Apex test classes — add to existing ones
- MUST NOT: Add eSignature template browsing in this phase — the operator inputs the template ID directly
- PREFER: Option A (template-based envelope) for
createEnvelope()— simpler, already decided - ESCALATE: If the eSignature named credential does not exist in the org, stop and flag it
- ESCALATE: If
Appraiser_Email__cis blank on the queried case, stop — do not silently send with a blank email - ESCALATE: If the spec does not cover a required decision — do not fill gaps silently
Dependencies
CLM_Account_Setting__mdt— providesESignature_Account_Id__c,ESignature_Rest_Named_Credential__c- DocuSign eSignature REST API v2.1 —
/v2.1/accounts/{accountId}/envelopes - Named credential (AcctDemo_NamedCreds or equivalent) — must exist in appraiser-dev
Known Challenges
- The named credential for eSignature REST may differ from the CLM named credential — check
CLM_Account_Setting__mdtforESignature_Rest_Named_Credential__c - DocuSign template roles: the
roleNamemust match the role name defined in the eSignature template — "Signer" is the default, but the actual template may use a different name. If tests fail with a 400 from DocuSign about role name, escalate. - LWC must use
async/awaitfor imperative Apex calls (not wire adapters) for the send action — matches the existing attach pattern
Rejected Approaches
| Rejected option | Why rejected |
|---|---|
| Option B (upload CLM document as envelope document) | More complex, requires downloading the blob and managing recipients in code. Option A with a matching template is simpler and sufficient. |
| Creating a new Apex test class for eSignature | Not needed — DocusignESignatureServiceTest already exists and is the right home |
| Automatic template selection via API | Out of scope for Phase 2 — operator inputs template ID directly |
6. Reference Materials
Existing code to learn from
CLMAdminService.persistDocGenResult()— the exact pattern to replicate forpersistEnvelopeResult()CLMAdminService.getCaseContext()— how case fields are queried and mapped to a DTOCLMDocGenCalloutTest— the HTTP mock pattern to follow inDocusignESignatureServiceTestclmDocGenWorkbench— the LWC pattern (async handlers, spinner, button enable/disable) to follow for the send button
Anti-patterns
- Do not use
HttpRequest.setBodyAsBlob()for the JSON envelope payload — usesetBody()withJSON.serialize() - Do not trust the
statusfield in the LWC to determine if all prior steps completed — check the specific field (e.g.,attachedFileContentDocumentId) that the button should gate on
7. Evaluation Design
Test cases
TC-001: createEnvelope() success path
Input: Valid caseId, valid accountCode, valid templateId; mock HTTP returns 201 with {"envelopeId":"abc-123","status":"sent","uri":"/envelopes/abc-123"}
Expected output: EnvelopeCreateResult.success = true, envelopeId = "abc-123", status = "sent"
Verification: System.assertEquals(true, result.success) in DocusignESignatureServiceTest
TC-002: createEnvelope() failure path
Input: Valid caseId/accountCode/templateId; mock HTTP returns 400 with error body
Expected output: EnvelopeCreateResult.success = false, errorMessage is non-blank
Verification: System.assertEquals(false, result.success) and System.assertNotEquals(null, result.errorMessage)
TC-003: persistEnvelopeResult() writes correct fields
Input: Existing case record, envelopeId = "test-env-id", envelopeStatus = "sent", envelopeUri = "/envelopes/test-env-id"
Expected output: Re-queried case has ESignature_Envelope_Id__c = "test-env-id", ESignature_Sent_At__c is non-null
Verification: Re-query case after call and assert in CLMAdminServiceTest
Last updated: 2026-04-09