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

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

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:

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:

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

@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