salesforce-appraiser-review.../PROJECT-SPEC.md

328 lines
14 KiB
Markdown

# 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": "<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 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_