# 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__c` and 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 - [ ] `clmDocGenWorkbench` exposes 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 ```bash 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, `@AuraEnabled` methods, `with sharing` - Follow CLM tracking pattern: service layer method → result object → persist to case in `CLMAdminService` - All HTTP callouts mocked in tests using `HttpCalloutMock` (see `CLMDocGenCalloutTest` for the pattern) - LWC: follow existing `clmDocGenWorkbench` patterns (wire adapters, async/await handlers, tracked properties) - Field API names use `__c` suffix; 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.xml` is updated to include the new fields (or already covers `CustomField` members) --- ### 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:** ```json POST /v2.1/accounts/{accountId}/envelopes { "templateId": "", "templateRoles": [ { "email": "", "name": "", "roleName": "Signer" } ], "status": "sent" } ``` **Result class:** `EnvelopeCreateResult` — inner class on `DocusignESignatureService`: - `Boolean success` - `String envelopeId` - `String status` - `String envelopeUri` - `String errorMessage` **Method signature:** ```apex @AuraEnabled(cacheable=false) public static EnvelopeCreateResult createEnvelope( Id caseId, String accountCode, String templateId ) ``` **Acceptance criteria:** - [ ] Method is `@AuraEnabled` and callable from LWC - [ ] Resolves `eSignatureAccountId` and `eSignatureRestNamedCredential` from `CLM_Account_Setting__mdt` via `accountCode` - [ ] Queries `Appraiser_Email__c`, `Appraiser_Name__c`, `Appraiser_Last_Name__c`, `Appraiser_Salutation__c` from the case - [ ] POSTs correct JSON body to the eSignature API - [ ] Returns `EnvelopeCreateResult` with `envelopeId` and `status` on success - [ ] Returns `EnvelopeCreateResult` with `success=false` and `errorMessage` on 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:** ```apex @AuraEnabled(cacheable=false) public static void persistEnvelopeResult( Id caseId, String envelopeId, String envelopeStatus, String envelopeUri ) ``` **Fields written:** - `ESignature_Envelope_Id__c` = envelopeId - `ESignature_Envelope_Status__c` = envelopeStatus - `ESignature_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__c` to 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 eSignatureEnvelopeId` - `String eSignatureEnvelopeStatus` - `String eSignatureEnvelopeUrl` - `Datetime eSignatureSentAt` - `Datetime eSignatureCompletedAt` **Acceptance criteria:** - [ ] `getCaseContext()` SOQL query includes all 5 fields - [ ] `CaseContext` DTO 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 `attachedFileContentDocumentId` is 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 `templateId` via a text input field in the UI **New UI elements:** - Text input: "eSignature Template ID" (maps to `templateId` parameter) - Button: "Send for Signature" - Status display: envelope ID, status, sent timestamp (read from refreshed `caseContext`) **Acceptance criteria:** - [ ] Button is disabled when `attachedFileContentDocumentId` is 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 - [ ] `DocusignESignatureServiceTest` covers `createEnvelope()` — at minimum: success path (mock HTTP 201), failure path (mock HTTP 400), missing template ID guard - [ ] `CLMAdminServiceTest` covers `persistEnvelopeResult()` — at minimum: fields written correctly, `ESignature_Sent_At__c` is 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__mdt` to resolve named credentials — never hardcode - **MUST:** Follow the `persistDocGenResult` pattern in `CLMAdminService` for `persistEnvelopeResult` - **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__c` is 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` — provides `ESignature_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__mdt` for `ESignature_Rest_Named_Credential__c` - DocuSign template roles: the `roleName` must 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/await` for 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 for `persistEnvelopeResult()` - `CLMAdminService.getCaseContext()` — how case fields are queried and mapped to a DTO - `CLMDocGenCalloutTest` — the HTTP mock pattern to follow in `DocusignESignatureServiceTest` - `clmDocGenWorkbench` — 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 — use `setBody()` with `JSON.serialize()` - Do not trust the `status` field 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_