Compare commits

..

13 Commits

Author SHA1 Message Date
paulh 66be5d83ff fix(ralph-loop): replace with three-tier session reset detection from master
Supersedes the simple polling version written this session. The master
harness version (from the other machine) has:
  Tier 1 — Anthropic API probe via ANTHROPIC_API_KEY if available
  Tier 2 — Parse reset time from agent output ("resets 11am America/New_York")
  Tier 3 — Seeded --session-ends timestamp argument
  Tier 4 — Fixed fallback sleep (--retry-wait, default 1800s)

Agent: human
Tests: N/A
Tests-Added: 0
TypeScript: N/A
2026-04-09 21:56:03 -04:00
paulh 82e10ff810 fix(ralph-loop): add session limit detection and auto-recovery
When claude -p hits the Pro subscription usage limit, the old loop had
no detection — it would find no <promise> signal, treat it as a normal
continuation, and immediately retry, burning all --max iterations.

New behaviour:
- check_output() returns status 4 when the log contains any usage/rate
  limit message (case-insensitive, multiple pattern variants)
- wait_for_session_reset() polls via a trivial probe call every
  SESSION_POLL_INTERVAL seconds (default: 600s / 10 min) until claude
  responds cleanly again
- When rate-limited, the same iteration is retried (i is not incremented)
  so no task is skipped or double-counted
- set -e is temporarily suspended around agent calls so a non-zero claude
  exit doesn't kill the bash process

Also updated the master template in docs/agent-harness/ralph-loop.sh.

Agent: human
Tests: N/A
Tests-Added: 0
TypeScript: N/A
2026-04-09 21:39:29 -04:00
paulh b1c199d21d chore: scaffold agent harness for Phase 2 eSignature envelope creation
Adds the ralph loop harness artifacts to drive autonomous implementation
of Phase 2. State lives in IMPLEMENTATION_PLAN.md so work survives
session resets — restart ralph-loop.sh after token refresh and the next
agent reads the plan to pick up where it left off.

Agent: human
Tests: N/A
Tests-Added: 0
TypeScript: N/A
2026-04-09 21:34:11 -04:00
paulh c49d127db1 Update FEATURES_UPDATE and NEXT_STEPS to reflect current state
FEATURES_UPDATE.md: rewrote from scratch — was a stub from February
describing only docs-writing activity. Now documents all implemented
features: CLM generation stack, account/letter config metadata, eSignature
browsing layer, infrastructure, and what is not yet built.

NEXT_STEPS_DOCGEN.md: expanded from CLM-launch-only scope to cover the
full Phase 2 eSignature envelope creation plan (tracking fields, createEnvelope
approaches, persistence, UI button) and Phase 3 additional letter types.

CURRENT_STATUS.md: fix two remaining absolute /home/paulh/... doc links.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 21:16:58 -04:00
paulh 546ffd7dcf Remove erroneous ClientCertificate from CLMs1NamedCreds
JWT signing is handled by the DocusignJWT external credential. The
ClientCertificate parameter was adding unnecessary mTLS configuration
that DocuSign's API does not require. Now matches CLMuatNamedCreds.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 21:11:25 -04:00
paulh 8fb9df01db Retrieve AcctDemo_NamedCreds into source; add to manifest
SecuredEndpoint credential pointing to account-d.docusign.com, backed by
the DocusignJWT external credential. Referenced by DTC_CLM_Demo account
setting for ESignature_Auth_Named_Credential__c.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 21:01:23 -04:00
paulh 091b1870b6 Fix AcctDemo_NamedCreds note — exists in org, not yet retrieved to source
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 20:56:06 -04:00
paulh 148854ff1e Add retrieved/ and .codex to .gitignore; document manual credential setup
.gitignore:
- Add retrieved/ (org retrieve scratch metadata — not source of truth)
- Add .codex (Claude Code agent state)

SALESFORCE_SETUP.md:
- Add AcctDemo_NamedCreds to named credentials list with note that it must
  be created manually in the target org (not in source)
- Add "Manual post-deploy steps" section documenting AcctDemo_NamedCreds
  setup and the DocusignJWT external credential demo values (iss/sub/aud)
  that need to be replaced before use in a real org

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 20:51:03 -04:00
paulh 0ce516bbfc Fix LWC async bug, null guard, stale sample script; update manifest
LWC fixes:
- clmDocGenWorkbench: resetSelectionsToDefaults was a sync onclick handler
  calling async initializeDefaults without await — isBusy not set and errors
  silently dropped; made the method async and added await
- clmRequestPreview: loadPreview called from refreshAll (account/letter change
  handlers) without a recordId guard; a null appraiserCaseId would propagate
  to the Apex AuraHandledException; added early return with a user-facing message

manifest/package.xml:
- Was missing CLM_Account_Setting__mdt and CLM_Letter_Definition__mdt objects
  and all 17 custom metadata records
- Missing DocusignESignatureService and its test class
- Missing docusignEsignWorkbench and clmRequestPreview LWC components
- Missing NamedCredential section entirely (CLMs1*, CLMuat*, Esignature_Demo)
- Missing CLM Account Setting and Letter Definition layouts

scripts/apex/createSampleAppraiserCase.apex:
- Rewrote entirely; old version referenced the deleted Appraiser_Deficiency__c
  object, Sort_Order__c, Deficiency_Number__c as a string, and the deleted
  AppraiserCaseDocGenService class
- Now uses Appraiser_Case_Deficiency__c with correct field types
- Populates all appraiser identity/address fields added since original version
- Debug output uses AppraiserCasePayloadBuilder and CLMDocGenCallout

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 20:50:57 -04:00
paulh fe337efe63 Update documentation to reflect current architecture
SALESFORCE_SETUP.md:
- Rewrite "What was added" to cover all current fields, metadata types,
  Apex classes, LWC components, named credentials, and permission sets
- Add DocusignESignatureServiceTest to the test run command

README.md:
- Replace absolute /home/paulh/... paths with relative ./FILE.md links

CLM_INTEGRATION.md:
- Pattern 1: replace wrong 3-arg trigger callout with correct Queueable pattern
  (triggers cannot make direct callouts)
- Pattern 2: mark LWC quick action as implemented (no longer a recommendation)
- Pattern 3: update REST resource to use CLMAdminService.generateDocument signature
- Payload/request/response: replace old pre-XML format with current
  documentxmlmergetasks JSON and XML structure
- Template design: replace Handlebars syntax with CLM XML merge description

NEXT_STEPS_DOCGEN.md:
- Mark Option B (LWC quick action) as complete
- Replace open-ended "what to confirm" with forward-looking guidance on
  extending to NOD, Education, and Intent to Remove letter types

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 20:36:29 -04:00
paulh 45814dc2d5 Add CLMAdminService, DocusignESignatureService, and fix code review issues
New classes:
- CLMAdminService: UI-facing orchestration — account/letter settings from
  metadata, document generation, task status polling, file attachment,
  folder browsing; persists all CLM results to Appraiser_Case__c
- DocusignESignatureService: eSignature API browsing (accounts, templates,
  envelopes, login info, user info)
- CLMAdminServiceTest, CLMDocGenCalloutTest, DocusignESignatureServiceTest

Updated classes (AppraiserCasePayloadBuilder, CLMDocGenCallout):
- CLMDocGenCallout: full XML merge callout stack, task status polling,
  document download, recursive document href discovery, account-based
  endpoint building; HTTP_TIMEOUT made public
- AppraiserCasePayloadBuilder: formatMailingAddress made public so
  CLMAdminService can reuse it rather than duplicating the logic

Code review bug fixes:
- Fix null fields emitting literal "null" in generated XML — add safeValue()
  helper; String.valueOf(null) returns "null" so escapeXml's null guard
  never fired
- Fix unguarded inline SOQL in getCaseContext and getDocGenPreview — throws
  QueryException for missing records instead of AuraHandledException; now
  uses list query with isEmpty guard
- Remove duplicate formatAddress in CLMAdminService; delegate to
  AppraiserCasePayloadBuilder.formatMailingAddress
- Replace hardcoded 30000 timeout in performGet with CLMDocGenCallout.HTTP_TIMEOUT
- Remove duplicate JSDoc on getTaskStatus

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 20:36:16 -04:00
paulh 62b78faf1a Add new Salesforce metadata for account-based CLM and eSignature integration
- Appraiser_Case__c: appraiser identity/address fields, FHA case number,
  CLM tracking fields (task ID/URL/status), generated document fields,
  attached file fields, Letter_Sent_Date__c
- Appraiser_Case_Deficiency__c: Reference__c field, blank-record validation rule
- CLM_Account_Setting__mdt and CLM_Letter_Definition__mdt: per-account CLM
  and eSignature configuration with seeded records for DTC_CLM_Demo,
  DTC_IAM_Enterprise, DTC_HUD_Demo
- CLM_Environment_Setting__mdt: UAT/S1 environment defaults
- Named credentials: CLMs1NamedCreds, CLMs1Download, Esignature_Demo_NamedCreds
- LWC: clmDocGenWorkbench, docusignEsignWorkbench, clmRequestPreview
- Layouts, record page, permission sets, quick action, app, tabs, manifest

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 20:35:59 -04:00
paulh 703fb0c0ba Remove legacy Appraiser_Deficiency__c object and AppraiserCaseDocGenService
Replaced by Appraiser_Case_Deficiency__c (master-detail, with Reference__c
field) and the XML-merge-based CLMDocGenCallout/CLMAdminService stack.
Also removes placeholder named credentials CLMNamedCred and CLMuatDownloadNamedCreds
superseded by the account-specific credential set.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 20:35:42 -04:00
141 changed files with 12830 additions and 596 deletions

6
.gitignore vendored
View File

@ -15,3 +15,9 @@ __pycache__/
# OS # OS
.DS_Store .DS_Store
Thumbs.db Thumbs.db
# Scratch retrieved metadata (org retrieves for reference only)
retrieved/
# Claude Code agent state
.codex

168
AGENT.md Normal file
View File

@ -0,0 +1,168 @@
# Agent Instructions — Salesforce Appraiser Review Letter
> Read this file at the start of every iteration. It is the canonical operating guide.
---
## Role
You are a senior Salesforce developer working autonomously on the appraiser review letter project.
You have full access to the codebase and can read, write, and run shell commands.
---
## Core Loop
### 1. Orient
- Read `PROJECT-SPEC.md` — requirements and acceptance criteria for Phase 2
- Read `IMPLEMENTATION_PLAN.md` — task list and current status
- Read `DECISIONS.md` — architecture decisions already in force
- Run `git log --oneline -10` — see what has been done
- Check for any prior escalations in `IMPLEMENTATION_PLAN.md`
### 2. Pick ONE Task
- Find the first unchecked task `- [ ]` in `IMPLEMENTATION_PLAN.md`
- If all tasks are checked, output `<promise>DONE</promise>` and exit
- Work only on this one task — do not pick up the next one
### 3. Implement
- Follow the existing code patterns in the project
- Read the relevant existing classes/LWCs before writing new code
- Study the CLM tracking pattern as the model for eSignature tracking
### 4. Verify — BLOCKING before commit
```bash
# Set up the CLI path (system Node v12 is too old — always use nvm Node)
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 source to org
$NODE $SF project deploy start \
--source-dir force-app \
--target-org appraiser-dev
# Run Apex tests (synchronous, human-readable output)
$NODE $SF apex run test \
--target-org appraiser-dev \
--class-names DocusignESignatureServiceTest,CLMAdminServiceTest,AppraiserCasePayloadBuilderTest,CLMDocGenCalloutTest \
--result-format human \
--synchronous
```
**Do not commit if deploy fails or tests fail.**
For metadata-only tasks (adding custom fields, no Apex changes): deploy and confirm the object deploys cleanly, but you do not need to run all test classes — run a smoke test against the affected test class only.
### 5. Commit and Mark Done
Commit with this exact trailer format:
```
<type>(<scope>): <description>
<optional body>
Agent: claude-sonnet-4-6
Tests: N/N passing | N/A (metadata-only)
Tests-Added: +N | 0
TypeScript: N/A (Apex project)
```
Then mark the task `- [x]` in `IMPLEMENTATION_PLAN.md` and commit that update in the same commit.
### 6. Exit
Output a brief summary (what you did, test results, what is next) and exit.
The loop will restart you with fresh context for the next task.
---
## Rules
1. **One task per iteration.** Never work on multiple tasks.
2. **Read before writing.** Always read the existing class/LWC before modifying it.
3. **Follow existing patterns.** The CLM tracking pattern is the model for eSignature tracking.
4. **Tests are mandatory for every Apex change.** Add to the existing test classes — do not create new ones unless the spec says to.
5. **Never add fields or methods not required by the spec.** Implement what is asked, nothing more.
6. **Never modify production data.** All DML in tests uses `@isTest` and `Test.startTest()/stopTest()`.
7. **Mock HTTP callouts in tests.** Use `HttpCalloutMock` — same pattern as existing test classes.
8. **Deploy before committing.** Never commit code that does not deploy cleanly.
9. **Follow DECISIONS.md.** Do not change approaches listed as Accepted without escalating.
10. **If stuck, escalate.** Do not invent missing requirements.
---
## Escalation Protocol
Stop and add this block at the top of `IMPLEMENTATION_PLAN.md`, then output `<promise>STUCK</promise>`:
```markdown
## ESCALATION REQUIRED
- **Task:** [current task]
- **Issue:** [what is ambiguous/missing/conflicting]
- **What I need:** [specific question or decision]
- **What I'd do if I had to guess:** [best guess]
```
Escalate when:
- The spec has no FR covering a required decision
- A constraint in DECISIONS.md conflicts with the spec
- A Salesforce platform limit or API behavior is unclear
- A named credential or metadata record is missing from the org
- Any task requires deleting or overwriting existing custom fields already in use
---
## Output Signals
- `<promise>PLANNED</promise>` — plan created, ready for build
- `<promise>DONE</promise>` — all tasks complete
- `<promise>STUCK</promise>` — needs human intervention
- `<promise>ERROR</promise>` — unrecoverable error
---
## Project Structure
```
force-app/main/default/
├── classes/
│ ├── CLMAdminService.cls ← orchestration + persistDocGenResult pattern
│ ├── CLMAdminServiceTest.cls ← test pattern to follow
│ ├── CLMDocGenCallout.cls
│ ├── CLMDocGenCalloutTest.cls
│ ├── DocusignESignatureService.cls ← add createEnvelope() here
│ ├── DocusignESignatureServiceTest.cls
│ ├── AppraiserCasePayloadBuilder.cls
│ └── AppraiserCasePayloadBuilderTest.cls
├── lwc/
│ ├── clmDocGenWorkbench/ ← add Send for Signature button here
│ └── docusignEsignWorkbench/ ← read-only browsing (do not modify Phase 2)
└── objects/
└── Appraiser_Case__c/
└── fields/ ← add 5 eSignature tracking fields here
```
---
## SF CLI Invocation (Always Use This Pattern)
```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
$NODE $SF <subcommand> <args>
```
Target org alias: `appraiser-dev`
---
## Context Anchor
- **Phase 1 is complete** — CLM doc generation works end-to-end. Do not modify it.
- **Phase 2 goal** — after a CLM doc is generated and attached, enable sending it for eSignature using a DocuSign template (Option A).
- **The authoritative plan** is in `NEXT_STEPS_DOCGEN.md` under Phase 2. `IMPLEMENTATION_PLAN.md` is the execution tracker derived from it.

122
DECISIONS.md Normal file
View File

@ -0,0 +1,122 @@
# Architecture Decision Records
> Non-obvious decisions that agents must not undo.
> Read during Orient phase every iteration.
---
### ADR-001: Option A (template-based envelope) for createEnvelope()
**Date:** 2026-04-09
**Status:** Accepted
**Context:**
Two approaches were considered for creating a DocuSign eSignature envelope:
- Option A: POST with a `templateId` — DocuSign template defines recipients/tabs, Salesforce only passes merge data at send time
- Option B: POST with a raw document (download CLM blob, upload as base64) — Salesforce manages all recipient config
**Decision:**
Use Option A. Simpler implementation, no blob download, recipient config lives in the DocuSign template where it belongs.
**Consequences:**
✅ Fewer lines of Apex (no ContentVersion download, no base64 encoding)
✅ Recipient/tab configuration lives in DocuSign where it is maintained
❌ Requires a matching eSignature template to exist in the DocuSign account
❌ If the template does not exist, the API returns a 400 — the operator must set up the template first
**Alternatives Considered:**
Option B was rejected as unnecessary complexity for this proof-of-concept phase.
---
### ADR-002: Named credentials for all DocuSign API calls
**Date:** 2026-04-09
**Status:** Accepted
**Context:**
All CLM and eSignature API calls must go through Salesforce named credentials. No hardcoded tokens, passwords, or base URLs in Apex.
**Decision:**
Named credential API name is always sourced from `CLM_Account_Setting__mdt`:
- CLM calls use `CLM_API_Named_Credential__c`
- eSignature calls use `ESignature_Rest_Named_Credential__c`
The `eSignatureAccountId` is also from `CLM_Account_Setting__mdt.ESignature_Account_Id__c`.
**Consequences:**
✅ No secrets in source code
✅ Works with Salesforce's credential management (OAuth token refresh, etc.)
❌ Named credentials must be manually configured in each org before the code works
---
### ADR-003: persistEnvelopeResult follows persistDocGenResult pattern
**Date:** 2026-04-09
**Status:** Accepted
**Context:**
The project already has `CLMAdminService.persistDocGenResult()` which writes CLM task results back to the case. The eSignature result persistence should follow the same pattern — not a new approach.
**Decision:**
`CLMAdminService.persistEnvelopeResult()` is an `@AuraEnabled` static method that takes structured parameters (not a blob or generic map) and writes specific fields to the case via a SOQL update. Same structure as `persistDocGenResult`.
**Consequences:**
✅ Consistent code pattern, agent can learn from existing method
✅ Testable with the same approach as `CLMAdminServiceTest`
---
### ADR-004: eSignature tracking fields belong on Appraiser_Case__c
**Date:** 2026-04-09
**Status:** Accepted
**Context:**
The case is the central record. CLM tracking fields already live on `Appraiser_Case__c`. The eSignature tracking fields follow the same pattern.
**Decision:**
All five eSignature lifecycle fields go on `Appraiser_Case__c`, not on a child record or a separate object.
**Consequences:**
✅ Single record for operator to view
✅ Consistent with existing CLM field placement
❌ Only one envelope tracked per case (sufficient for Phase 2)
---
### ADR-005: Operator manually enters template ID in Phase 2
**Date:** 2026-04-09
**Status:** Accepted
**Context:**
Option: auto-populate the template ID from `CLM_Letter_Definition__mdt` or present a template browser.
Both require additional metadata fields or another API call and are out of scope for Phase 2.
**Decision:**
The operator inputs the eSignature template ID directly into a text field in `clmDocGenWorkbench`. No auto-selection, no browser, no additional metadata field.
**Consequences:**
✅ Simpler Phase 2 scope
✅ No new metadata record changes needed
❌ Operator must know the template ID — acceptable for a proof-of-concept
**Review after:**
Phase 3 could add a `ESignature_Template_Id__c` field to `CLM_Letter_Definition__mdt` and pre-populate the input.
---
### ADR-006: HTTP mocks required for all Apex callout tests
**Date:** 2026-04-09
**Status:** Accepted
**Context:**
Salesforce requires all HTTP callouts in test classes to use `Test.setMock(HttpCalloutMock.class, ...)`. The existing test classes all follow this pattern.
**Decision:**
All tests for `createEnvelope()` must mock the HTTP response using an inner `MockHttpResponse` class implementing `HttpCalloutMock`. Do not attempt live callouts in test methods.
**Consequences:**
✅ Tests run in any Salesforce org without external connectivity
✅ Consistent with existing test class patterns (see `CLMDocGenCalloutTest`)
---
_Decisions don't drift if they're written down._

33
IMPLEMENTATION_PLAN.md Normal file
View File

@ -0,0 +1,33 @@
# Implementation Plan — Phase 2: eSignature Envelope Creation
> This is the live execution tracker. The agent reads and updates this every iteration.
> Check off tasks as they are completed (`- [x]`).
> Add `ESCALATION REQUIRED` block at the top if stuck.
---
## Tasks
- [ ] **Task 1 — Custom fields:** Add 5 eSignature tracking fields to `Appraiser_Case__c` object metadata and update `package.xml` if needed. Deploy to verify. (FR-001)
- [ ] **Task 2 — EnvelopeCreateResult class + createEnvelope() method:** Add `EnvelopeCreateResult` inner class and `createEnvelope(Id caseId, String accountCode, String templateId)` to `DocusignESignatureService`. (FR-002)
- [ ] **Task 3 — Tests for createEnvelope():** Add test coverage in `DocusignESignatureServiceTest` — success path (mock 201), failure path (mock 400), blank email guard. (NFR-001, TC-001, TC-002)
- [ ] **Task 4 — persistEnvelopeResult() + CaseContext extension:** Add `persistEnvelopeResult()` to `CLMAdminService`. Extend `CaseContext` inner class and `getCaseContext()` SOQL to include the 5 eSignature fields. (FR-003, FR-004)
- [ ] **Task 5 — Tests for persistEnvelopeResult():** Add test coverage in `CLMAdminServiceTest` — verify fields written correctly, `ESignature_Sent_At__c` is set, `ESignature_Completed_At__c` is NOT written on a "sent" result. (NFR-001, TC-003)
- [ ] **Task 6 — clmDocGenWorkbench UI:** Add "eSignature Template ID" text input and "Send for Signature" button to `clmDocGenWorkbench`. Button calls `createEnvelope()` then `persistEnvelopeResult()`, shows result. (FR-005)
- [ ] **Task 7 — Full deploy and test run:** Deploy entire change set, run all four test classes, verify all pass. Confirm no regressions. Update `docs/CURRENT_STATUS.md` to reflect Phase 2 completion.
---
## Completion
When all tasks are checked, output `<promise>DONE</promise>`.
---
_Phase 2 plan created: 2026-04-09_

327
PROJECT-SPEC.md Normal file
View File

@ -0,0 +1,327 @@
# 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_

View File

@ -1,5 +1,7 @@
# CLM Doc Gen Integration Guide # CLM Doc Gen Integration Guide
> Status note (2026-04-07): the repo is now standardized on the XML merge path built on `AppraiserCasePayloadBuilder` and `CLMDocGenCallout`, using `Appraiser_Case_Deficiency__c` as the canonical child object. See `CURRENT_STATUS.md` before using this guide as the sole source of truth.
## Overview ## Overview
This guide explains how to integrate Salesforce with DocuSign CLM to generate Appraiser Review Letters dynamically from Appraiser Case records. This guide explains how to integrate Salesforce with DocuSign CLM to generate Appraiser Review Letters dynamically from Appraiser Case records.
@ -80,42 +82,57 @@ Recipient Email
## Usage Patterns ## Usage Patterns
### Pattern 1: Apex Trigger (Automatic) ### Pattern 1: Queueable Apex (Automatic/Trigger-Driven)
**Scenario**: Generate letter automatically when Appraiser Case status reaches "Ready for Review" **Scenario**: Generate letter automatically from a trigger or process (callouts cannot be made synchronously from triggers — use a queueable)
```apex ```apex
// In a trigger on Appraiser_Case__c AFTER UPDATE public class AppraiserCaseDocGenJob implements Queueable, Database.AllowsCallouts {
if (oldMap.get(record.Id).Status__c != 'Ready for Review' && private Id caseId;
record.Status__c == 'Ready for Review') { private String accountCode;
private String templateDocHref;
private String destinationFolderHref;
private String destinationDocName;
CLMDocGenCallout.CLMDocGenResponse response = CLMDocGenCallout.generateDocument( public AppraiserCaseDocGenJob(Id caseId, String accountCode,
record.Id, String templateDocHref, String destinationFolderHref, String destinationDocName) {
'TEMPLATE_ID_FROM_CLM', // e.g., '123456' this.caseId = caseId;
record.Reviewer_Email__c this.accountCode = accountCode;
this.templateDocHref = templateDocHref;
this.destinationFolderHref = destinationFolderHref;
this.destinationDocName = destinationDocName;
}
public void execute(QueueableContext ctx) {
CLMDocGenCallout.CLMDocGenResponse response = CLMAdminService.generateDocument(
caseId, templateDocHref, destinationFolderHref, destinationDocName, accountCode
); );
if (!response.success) { if (!response.success) {
// Log error or send notification
System.debug('CLM Doc Gen failed: ' + response.message); System.debug('CLM Doc Gen failed: ' + response.message);
} }
} }
}
// Enqueue from a trigger:
System.enqueueJob(new AppraiserCaseDocGenJob(
record.Id,
'DTC_CLM_Demo',
letterSettings.defaultTemplateDocumentHref,
letterSettings.destinationRootFolderHref,
'Review_' + record.Name + '.docx'
));
``` ```
### Pattern 2: Flow (UI-Driven) ### Pattern 2: LWC Quick Action (UI-Driven) ✅ Implemented
**Scenario**: User clicks button to generate letter on-demand The `clmDocGenWorkbench` component, launched from the **Generate Review Letter** quick action, is the current implemented UI path. It calls `CLMAdminService` to:
- Browse available accounts and letter types
- Browse CLM template and destination folders
- Submit a merge task via `generateDocument()`
- Poll task status via `getTaskStatus()`
- Attach the generated file to the case via `attachGeneratedDocumentToCase()`
1. Create a Record-Triggered Flow on Appraiser_Case__c No additional configuration is needed beyond deploying the metadata and setting up Named Credentials.
2. Add an Action step:
- Action Type: "Apex Action"
- Apex Class: Choose custom Apex action that wraps `CLMDocGenCallout.generateDocument()`
3. Pass:
- recordId (the Appraiser Case)
- templateId (hardcoded or allow user to select)
- recipientEmail (from record or user input)
4. On success: Show toast, store document URL on record
5. On error: Show error message
### Pattern 3: REST API (External System) ### Pattern 3: REST API (External System)
@ -125,12 +142,16 @@ if (oldMap.get(record.Id).Status__c != 'Ready for Review' &&
@RestResource(urlMapping='/appraiser-case-generate-letter') @RestResource(urlMapping='/appraiser-case-generate-letter')
global class AppraiserCaseDocGenRest { global class AppraiserCaseDocGenRest {
@HttpPost @HttpPost
global static void generateLetter(String caseId, String templateId, String recipientEmail) { global static void generateLetter(
CLMDocGenCallout.CLMDocGenResponse response = CLMDocGenCallout.generateDocument( String caseId,
caseId, templateId, recipientEmail String accountCode,
String templateDocHref,
String destinationFolderHref,
String destinationDocName
) {
CLMDocGenCallout.CLMDocGenResponse response = CLMAdminService.generateDocument(
(Id) caseId, templateDocHref, destinationFolderHref, destinationDocName, accountCode
); );
// Return response
RestContext.response.statusCode = response.success ? 200 : 400; RestContext.response.statusCode = response.success ? 200 : 400;
RestContext.response.responseBody = Blob.valueOf(JSON.serialize(response)); RestContext.response.responseBody = Blob.valueOf(JSON.serialize(response));
} }
@ -141,97 +162,72 @@ global class AppraiserCaseDocGenRest {
## Payload Structure ## Payload Structure
### Input ### AppraiserCasePayloadBuilder output (intermediate JSON)
```json ```json
{ {
"AppraiserCaseNumber": "AC-00001", "AppraiserCaseNumber": "AC-000001",
"AppraiserFieldReviewDate": "2026-04-02", "AppraiserFieldReviewDate": "Apr 02, 2026",
"PropertyAddress": "123 Main St, Denver, CO 80202", "LetterSentDate": "Apr 09, 2026",
"FHACaseNumber": "123-4567890",
"AppraiserName": "Jamie Appraiser",
"AppraiserSalutation": "Ms.",
"AppraiserLastName": "Appraiser",
"AppraiserEmail": "jamie@example.com",
"AppraiserAddress": "245 Lexington Ave, New York, NY 10016, USA",
"PropertyAddress": "123 Main St, Denver, CO 80202, USA",
"DeficiencyList": [ "DeficiencyList": [
{ { "deficiencyNumber": 1, "description": "...", "resolution": "...", "reference": "VC-1" }
"deficiencyNumber": 1,
"description": "Missing comparable sale adjustment detail.",
"resolution": "Added adjustment rationale and supporting calculations."
},
{
"deficiencyNumber": 2,
"description": "Neighborhood trend explanation insufficient.",
"resolution": "Expanded market trend narrative with MLS evidence."
},
{
"deficiencyNumber": 3,
"description": "Photo date stamps were not included.",
"resolution": "Re-uploaded photos with date metadata and captions."
}
] ]
} }
``` ```
### CLM API Request (what CLMDocGenCallout sends) ### CLM API Request (what CLMDocGenCallout sends to `/documentxmlmergetasks`)
```json ```json
{ {
"templateId": "TEMPLATE_ID_FROM_CLM", "TemplateDocument": { "Href": "https://.../documents/<template-guid>" },
"mergeData": { ...payload above... }, "DataXML": "<TemplateFieldData><AppraiserCaseNumber>AC-000001</AppraiserCaseNumber>...<DeficiencyList><Deficiency><Number>1</Number>...</Deficiency></DeficiencyList></TemplateFieldData>",
"delivery": { "DestinationDocumentName": "Review_AC-000001.docx",
"recipientEmail": "reviewer@example.com", "DestinationFolder": { "Href": "https://.../folders/<folder-guid>" }
"documentName": "AppraiserReviewLetter_1743724800000"
},
"metadata": {
"salesforceRecordId": "a0wKW000007OIiCYAW",
"generatedAt": "2026-04-02T05:27:44Z"
}
} }
``` ```
### CLM API Response (on success) ### CLM API Response (on success)
```json ```json
{ {
"success": true, "Href": "https://apiuatna11.springcm.com/v2/<account-id>/documentxmlmergetasks/<task-guid>",
"documentUrl": "https://clm-instance.docusign.com/documents/ABC123XYZ", "Status": "Queued",
"documentId": "DOC-001", "Result": { "Href": "https://apiuatna11.springcm.com/v2/<account-id>/documents/<doc-guid>" }
"message": "Document generated successfully"
} }
``` ```
The response is persisted to `Appraiser_Case__c` tracking fields by `CLMAdminService.persistDocGenResult()`.
--- ---
## CLM Template Design ## CLM Template Design
### Template Merge Tags (Handlebars syntax) The implementation uses the DocuSign CLM **XML merge** (`documentxmlmergetasks`) API, not a Handlebars/template-key approach. Template field tags in the `.docx` file use CLM's Word merge syntax (typically `«FieldName»` merge fields or CLM repeat-block markers). Refer to your CLM tenant documentation for the exact syntax.
**Flat fields**: **Flat field example** (Word merge field in the .docx):
```handlebars ```
<p>Case Number: {{AppraiserCaseNumber}}</p> «AppraiserCaseNumber» «AppraiserFieldReviewDate»
<p>Review Date: {{AppraiserFieldReviewDate}}</p> «PropertyAddress»
<p>Property: {{PropertyAddress}}</p>
``` ```
**Deficiency repeat block**: **Deficiency repeat block** — the XML structure sent is:
```handlebars ```xml
<table> <DeficiencyList>
<tr> <Deficiency>
<th>Deficiency #</th> <Number>1</Number>
<th>Description</th> <Description>Missing comparable sale adjustment detail.</Description>
<th>Resolution</th> <Resolution>Added adjustment rationale and supporting calculations.</Resolution>
</tr> <Reference>VC-1</Reference>
{{#each DeficiencyList}} </Deficiency>
<tr> </DeficiencyList>
<td>{{deficiencyNumber}}</td> <DeficiencyCount>1</DeficiencyCount>
<td>{{description}}</td>
<td>{{resolution}}</td>
</tr>
{{/each}}
</table>
``` ```
**Conditional (if no deficiencies)**: Configure a repeat region in your CLM template over `DeficiencyList/Deficiency`, binding `Number`, `Description`, `Resolution`, and `Reference` within the loop.
```handlebars
{{#if DeficiencyList.length}}
<!-- Deficiency table -->
{{else}}
<p>No deficiencies found.</p>
{{/if}}
```
--- ---

View File

@ -1,6 +1,6 @@
# DocuSign CLM Template Mapping Snippet # DocuSign CLM Template Mapping Snippet
This project assumes Salesforce is the system of record and DocuSign CLM doc gen receives a payload built from `Appraiser_Case__c` and its related `Appraiser_Deficiency__c` records. This document reflects an older JSON-oriented payload experiment and is retained only as historical reference. The current working implementation uses `Appraiser_Case__c` and `Appraiser_Case_Deficiency__c` through the XML merge path.
## Suggested Merge Payload ## Suggested Merge Payload
@ -66,6 +66,5 @@ Use the actual DocuSign CLM token syntax used in your tenant, but the field mode
## Notes ## Notes
- Keep `deficiencies` as a repeatable child collection, not a flattened text blob. - Keep deficiency rows as repeatable child data, not a flattened text blob.
- If DocuSign CLM requires a REST callout payload, `AppraiserCaseDocGenService.buildDocGenRequestJson()` is a good source payload to hand to your integration layer. - The active implementation in this repo now uses `AppraiserCasePayloadBuilder` plus `CLMDocGenCallout` rather than `AppraiserCaseDocGenService`.
- If your CLM tenant uses a different collection token syntax, map the same logical field names there.

57
docs/CURRENT_STATUS.md Normal file
View File

@ -0,0 +1,57 @@
# Current Status — 2026-04-09
## Executive summary
The project is now a working Salesforce proof of concept for both DocuSign CLM document generation and basic eSignature API exploration. The app is standardized on `Appraiser_Case__c` plus `Appraiser_Case_Deficiency__c`, uses account-based DocuSign configuration through `CLM_Account_Setting__mdt`, and provides two operator-facing LWCs:
- `clmDocGenWorkbench` for CLM document generation
- `docusignEsignWorkbench` for eSignature API browsing
## What is implemented
- canonical Salesforce parent object:
- `Appraiser_Case__c`
- canonical Salesforce child object:
- `Appraiser_Case_Deficiency__c`
- CLM merge payload builder:
- `AppraiserCasePayloadBuilder`
- CLM callout service:
- `CLMDocGenCallout`
- CLM UI orchestration:
- `CLMAdminService`
- CLM quick-action workbench:
- `clmDocGenWorkbench`
- eSignature service layer:
- `DocusignESignatureService`
- eSignature record-page workbench:
- `docusignEsignWorkbench`
- account-based config through:
- `CLM_Account_Setting__mdt`
- letter-definition config through:
- `CLM_Letter_Definition__mdt`
- task and generated-document tracking on `Appraiser_Case__c`
- generated-document attachment to Salesforce Files
- Apex tests for payload generation, CLM callouts/admin service, and eSignature service
## What is working
- generating CLM review letters from a Salesforce record page
- selecting letter types through metadata-backed defaults while preserving the current appraiser letter
- browsing CLM templates and destination folders by configured account
- polling CLM task status
- attaching generated CLM documents back to the case
- browsing eSignature login info, user info, accounts, templates, and envelopes from Salesforce UI
## What is still not finished
- only the current appraiser review letter is seeded; the additional three letters still need template records and CLM template build-out
- no envelope creation/send workflow yet
- no template detail or envelope detail UI yet
- no long-term productized admin/config UI beyond Salesforce Setup
- some historical docs remain useful but are not authoritative
## Current source of truth
- product-level summary:
- [PRODUCT_SPEC.md](./PRODUCT_SPEC.md)
- setup and environment notes:
- [SALESFORCE_SETUP.md](./SALESFORCE_SETUP.md)
- implementation:
- Apex classes and LWCs in `force-app/main/default`
## Immediate recommendation
Treat the codebase as a solid platform baseline. The next phase should focus on product enhancements rather than architecture cleanup: better eSignature workflows, richer record-page UX, and clearer admin/operational controls.

View File

@ -1,19 +1,64 @@
# Features / Change Log — Appraiser Review Letter # Features / Change Log — Appraiser Review Letter
## Progress Summary ## Current state (2026-04-09)
- CLM_TEMPLATE_GUIDE.md: Initial merge/tag logic and edge cases outlined
- requirements.md and design.md: Created and populated with core requirements and structure The project is a working Salesforce proof of concept covering both DocuSign CLM
- README.md: Project intro and architecture brief document generation and eSignature API exploration. All items below are deployed
- DEPLOYMENT_AND_TESTING.md: Deployment steps and testing workflow drafted and functional.
### CLM document generation
- `Appraiser_Case__c` with full appraiser identity, address, FHA case number,
letter sent date, and CLM tracking fields
- `Appraiser_Case_Deficiency__c` child object with number, description,
resolution, reference, and blank-record validation
- `AppraiserCasePayloadBuilder` — transforms case + deficiencies into CLM merge
payload (JSON and XML)
- `CLMDocGenCallout` — XML merge task submission, task status polling, document
download via separate download credential
- `CLMAdminService` — UI orchestration layer: account/letter settings, generate,
poll, attach, folder browse; persists all CLM results back to the case
- `clmDocGenWorkbench` LWC quick action — full generate → poll → attach workflow
with folder browsing, template selection, and letter type selection
- `clmRequestPreview` LWC — developer/debug view of the XML merge payload and
request body for a given case
### Account-based configuration
- `CLM_Account_Setting__mdt` — per-account CLM and eSignature configuration
(named credentials, folder hrefs, template hrefs, account IDs)
- `CLM_Letter_Definition__mdt` — per-account letter type definitions with
optional folder/template overrides; fallback chain to account defaults
- Three active accounts: DTC_CLM_Demo (UAT), DTC_IAM_Enterprise (S1),
DTC_HUD_Demo (UAT)
- Four letter types seeded (APPRAISER_REVIEW fully configured; NOD, Education,
Intent to Remove defined but CLM templates not yet built)
### eSignature API exploration
- `DocusignESignatureService` — read-only eSignature API calls: login info,
user info, account list, templates, envelopes
- `docusignEsignWorkbench` LWC — browse accounts, templates, and recent
envelopes from the Salesforce record page
- `AcctDemo_NamedCreds` + `DocusignJWT` external credential — JWT bearer auth
for eSignature account server calls
- `Esignature_Demo_NamedCreds` — REST API calls (templates, envelopes)
### Infrastructure
- Named credentials: CLMuatNamedCreds, CLMs1NamedCreds, CLMuatDownload,
CLMs1Download, Esignature_Demo_NamedCreds, AcctDemo_NamedCreds
- External credential: DocusignJWT (JWT bearer, RS256, account-d.docusign.com)
- Permission sets: Appraiser_Case_Admin, Appraiser_Case_Access
- Record page, quick action, custom app, tabs, layouts
--- ---
## Next Steps ## What is not yet built
- Expand template engine features (nested conditionals, richer formatting)
- Clarify integration specifics with Salesforce CLM - Envelope creation and send workflow (eSignature)
- Add more actionable questions in doc footers - Envelope/template detail drill-down UI
- eSignature activity tracking on `Appraiser_Case__c` (envelope ID, status,
sent/completed dates)
- CLM template build-out for NOD, Education, and Intent to Remove letter types
- Admin UI for configuration (currently managed through Salesforce Setup)
--- ---
_Last updated: 2026-02-26 13:28 PM_ _Last updated: 2026-04-09_
_Work in progress: Feature/requirement expansion and blockages/questions to be listed here._

View File

@ -1,34 +1,91 @@
# Next Steps — DocuSign CLM Launch Path # Next Steps
I added a placeholder Quick Action metadata file so there is an explicit place in the project for the document generation launch pattern. ## What is done
## Reality check The CLM document generation workflow is fully implemented:
- `clmDocGenWorkbench` quick action — account selection, letter type selection,
folder browsing, template selection, generate, poll, attach
- `CLMAdminService` + `CLMDocGenCallout` — full Apex service layer
- All CLM tracking fields persisted back to `Appraiser_Case__c`
DocuSign CLM launch configuration varies by package and org setup. Because of that, the action in this repo is a scaffold/placeholder, not a guaranteed final production action. The eSignature browsing layer is implemented (read-only):
- `docusignEsignWorkbench` — accounts, templates, envelopes
- `DocusignESignatureService` — login info, user info, account list, templates,
envelopes
## Recommended implementation path ---
### Option A — Screen Flow (recommended first) ## Phase 2 — eSignature envelope creation
- Create a Screen Flow or autolaunched Flow that accepts `recordId`
- Call an Apex invocable or Apex action that builds the payload
- Hand that payload to your DocuSign CLM mechanism
- Redirect user to resulting document or status page
### Option B — LWC / Aura quick action The natural next step is connecting the two halves: after a CLM document is
- Use a Lightning Web Component quick action on `Appraiser_Case__c` generated and attached to the case, send it for signature.
- Call `AppraiserCaseDocGenService.buildDocGenRequestJson(recordId, templateKey)`
- Send the payload to the installed DocuSign CLM endpoint or orchestration layer
### Option C — Button / URL hack ### 1. Add eSignature tracking fields to `Appraiser_Case__c`
- Usually fast, usually brittle. I dont recommend it unless your CLM package explicitly documents it.
## What to confirm in your org Fields needed on the case to track the envelope lifecycle:
- `ESignature_Envelope_Id__c` (Text)
- `ESignature_Envelope_Status__c` (Text — Created / Sent / Delivered / Completed / Voided)
- `ESignature_Sent_At__c` (DateTime)
- `ESignature_Completed_At__c` (DateTime)
- `ESignature_Envelope_Url__c` (URL — link to envelope in DocuSign)
1. Exact DocuSign CLM package/API available in Salesforce ### 2. Add `createEnvelope()` to `DocusignESignatureService`
2. Whether generation is initiated by package component, Flow action, Apex callout, or named credential call
3. Template identifier format (`templateKey`, template Id, or external document key)
4. Returned artifact behavior (attach to record, email, save to CLM repository, etc.)
## Good next move Two approaches depending on workflow preference:
Once you know the exact DocuSign package artifact available in the org, I can wire the placeholder into a real Flow/LWC/Apex launch path. **Option A — Envelope from template**
Use a pre-built eSignature template. Recipients and document are defined in the
template; Salesforce passes merge data (tabs) at send time.
```apex
// POST /v2.1/accounts/{accountId}/envelopes
// body: { templateId, templateRoles: [{ email, name, roleName }], status: 'sent' }
```
**Option B — Envelope from uploaded document**
Use the CLM-generated document (already attached to the case as a
ContentVersion). Download the blob and POST it directly as an envelope document.
```apex
// POST /v2.1/accounts/{accountId}/envelopes
// body: { documents: [{ documentBase64, name, fileExtension, documentId }],
// recipients: { signers: [...] }, status: 'sent' }
```
Option A is simpler if a matching eSignature template exists.
Option B gives more control but requires managing recipient configuration in code.
### 3. Persist envelope result to `Appraiser_Case__c`
On success, write the envelope ID, status, and sent timestamp back to the case
(same pattern as `CLMAdminService.persistDocGenResult`).
### 4. Add send action to `clmDocGenWorkbench`
After "Attach Generated Document" succeeds, enable a "Send for Signature" button
that calls the new `createEnvelope()` method. Show envelope status alongside
the existing task status display.
### 5. Add envelope status polling (optional)
Mirror the CLM task status pattern: a "Check Signature Status" button that calls
`GET /v2.1/accounts/{accountId}/envelopes/{envelopeId}` and updates the case.
---
## Phase 3 — Additional letter types
Three letter types are defined in `CLM_Letter_Definition__mdt` but have no CLM
templates yet:
- `NOD_LETTER` — Notice of Deficiency
- `EDUCATION_LETTER` — Education correspondence
- `INTENT_TO_REMOVE_LETTER` — Intent to Remove notification
For each:
1. Build the CLM template `.docx` and upload to the CLM account
2. Update the `Default_Template_Document_Href__c` in the corresponding
`CLM_Letter_Definition__mdt` records
3. Confirm whether deficiency display or field set differs from the appraiser
review letter (if so, extend `AppraiserCasePayloadBuilder`)
---
_Last updated: 2026-04-09_

291
docs/PRODUCT_SPEC.md Normal file
View File

@ -0,0 +1,291 @@
# Product Spec — Appraiser Review Letter Platform
## Purpose
This project is a Salesforce-based operator tool for generating appraiser review letters in DocuSign CLM from `Appraiser_Case__c` data, while also serving as a proof-of-concept platform for broader DocuSign API integrations across CLM and eSignature.
## Product goals
- Let a user open an `Appraiser Case` in Salesforce and generate a review letter in DocuSign CLM without using Execute Anonymous.
- Keep CLM account, folder, template, and destination selection configurable per account.
- Persist generation status and generated-document references back to Salesforce.
- Allow operators to browse and test eSignature API data from the same Salesforce app.
- Provide a clean foundation for future workflows such as template selection, envelope creation, and richer document operations.
## Primary users
- Salesforce admins configuring DocuSign connectivity and defaults
- Business users generating review letters from `Appraiser Case` records
- Technical users validating CLM and eSignature API behavior in a safe UI
## Current in-scope workflows
### 1. CLM document generation
- User opens an `Appraiser Case` record.
- User launches the `Generate Review Letter` quick action.
- User selects a configured CLM account.
- User selects a configured letter type.
- User browses CLM folders and templates or uses saved defaults.
- User generates a document through CLM `documentxmlmergetasks`.
- User checks task status.
- User attaches the generated CLM document back to Salesforce as a File.
### 2. eSignature API browsing
- User opens an `Appraiser Case` record.
- User navigates to the `Docusign eSignature` tab on the record page.
- User selects a configured account.
- User loads:
- login information
- OAuth user info
- discovered eSignature accounts
- account templates
- recent envelopes
## Canonical Salesforce data model
### Appraiser Case
`Appraiser_Case__c` is the primary business record.
Current functional fields include:
- `Name`
- `Appraiser_Field_Review_Date__c`
- `Letter_Sent_Date__c`
- `FHA_Case_Number__c`
- `Appraiser_Name__c`
- `Appraiser_Last_Name__c`
- `Appraiser_Street__c`
- `Appraiser_City__c`
- `Appraiser_State_Province__c`
- `Appraiser_Postal_Code__c`
- `Appraiser_Country__c`
- `Property_Street__c`
- `Property_City__c`
- `Property_State_Province__c`
- `Property_Postal_Code__c`
- `Property_Country__c`
Current CLM tracking fields include:
- `Last_CLM_Account_Code__c`
- `Last_DocGen_Status__c`
- `Last_DocGen_Message__c`
- `Last_DocGen_Task_Id__c`
- `Last_DocGen_Task_Url__c`
- `Generated_Document_Id__c`
- `Generated_Document_Url__c`
- `Last_DocGen_Requested_At__c`
- `Last_DocGen_Completed_At__c`
- `Last_Template_Document_Href__c`
- `Last_Destination_Folder_Href__c`
- `Attached_File_Content_Document_Id__c`
- `Attached_File_Url__c`
### Appraiser Case Deficiency
`Appraiser_Case_Deficiency__c` is the canonical child object.
Fields:
- `Appraiser_Case__c`
- `Deficiency_Number__c`
- `Description__c`
- `Reference__c`
- `Resolution__c`
Validation:
- blank deficiency records are blocked by validation
### CLM account configuration
`CLM_Account_Setting__mdt` is the account-level configuration source of truth.
Fields include:
- account identity
- `Account_Code__c`
- `Account_Display_Name__c`
- `Environment_Code__c`
- `Active__c`
- CLM configuration
- `CLM_Account_Id__c`
- `CLM_Api_Named_Credential__c`
- `CLM_Download_Named_Credential__c`
- `Template_Root_Folder_Href__c`
- `Destination_Root_Folder_Href__c`
- `Default_Template_Document_Href__c`
- `Default_Destination_Document_Name_Prefix__c`
- eSignature configuration
- `ESignature_Auth_Named_Credential__c`
- `ESignature_Rest_Named_Credential__c`
- `ESignature_Account_Id__c`
Current active example accounts:
- `DTC CLM Demo`
- `DTC IAM Enterprise`
- `DTC HUD Demo`
### Letter definition configuration
`CLM_Letter_Definition__mdt` is the extensibility layer for letter types.
Each record can define:
- account code
- letter code
- display name
- default flag
- sort order
- default template href
- template root folder href
- destination root folder href
- default filename prefix
Current seeded letter definition:
- `APPRAISER_REVIEW`
Design intent:
- keep the current appraiser review letter working
- allow three additional letters to be added without redesigning the workbench
- support more letters later by metadata rather than code branching
## Canonical CLM architecture
### Payload generation
[AppraiserCasePayloadBuilder.cls](/home/paulh/.openclaw/workspace/projects/salesforce-appraiser-review-letter/force-app/main/default/classes/AppraiserCasePayloadBuilder.cls) builds the document payload from `Appraiser_Case__c` and `Appraiser_Case_Deficiency__c`.
Current payload fields:
- `AppraiserCaseNumber`
- `AppraiserFieldReviewDate`
- `LetterSentDate`
- `FHACaseNumber`
- `AppraiserName`
- `AppraiserLastName`
- `AppraiserStreet`
- `AppraiserCity`
- `AppraiserStateProvince`
- `AppraiserPostalCode`
- `AppraiserCountry`
- `AppraiserAddress`
- `PropertyStreet`
- `PropertyCity`
- `PropertyStateProvince`
- `PropertyPostalCode`
- `PropertyCountry`
- `PropertyAddress`
- `DeficiencyList[]`
### CLM callouts
[CLMDocGenCallout.cls](/home/paulh/.openclaw/workspace/projects/salesforce-appraiser-review-letter/force-app/main/default/classes/CLMDocGenCallout.cls) is the canonical CLM integration class.
Current supported operations:
- submit `documentxmlmergetasks`
- poll task status
- browse folders/documents through generic resource requests
- download generated documents
### CLM orchestration
[CLMAdminService.cls](/home/paulh/.openclaw/workspace/projects/salesforce-appraiser-review-letter/force-app/main/default/classes/CLMAdminService.cls) is the UI-facing orchestration layer.
Current supported operations:
- account selection and defaults
- case context retrieval
- folder/document browsing
- document generation
- task polling
- generated document attachment to Salesforce Files
### CLM UI
[clmDocGenWorkbench](/home/paulh/.openclaw/workspace/projects/salesforce-appraiser-review-letter/force-app/main/default/lwc/clmDocGenWorkbench/clmDocGenWorkbench.js) is the current operator UI.
Current behavior:
- launched from the `Generate Review Letter` quick action
- uses account-based CLM config
- uses account + letter-type selection
- supports template browsing
- supports destination browsing
- supports destination filename selection
- shows case deficiencies before generation
- shows task details and file attachment status
## Canonical eSignature architecture
### eSignature service layer
[DocusignESignatureService.cls](/home/paulh/.openclaw/workspace/projects/salesforce-appraiser-review-letter/force-app/main/default/classes/DocusignESignatureService.cls) is the current eSignature integration layer.
Current supported operations:
- `getAccountConfig`
- `getLoginInformation`
- `getUserInfo`
- `listAccounts`
- `getAccountInformation`
- `listTemplates`
- `listEnvelopes`
### eSignature UI
[docusignEsignWorkbench](/home/paulh/.openclaw/workspace/projects/salesforce-appraiser-review-letter/force-app/main/default/lwc/docusignEsignWorkbench/docusignEsignWorkbench.js) is the current exploratory UI.
Current behavior:
- placed on its own `Docusign eSignature` record tab
- supports account selection
- shows raw login info and user info
- shows discovered eSignature accounts
- shows templates
- shows recent envelopes filtered by `from_date`
## Named credential model
### CLM
- UAT CLM API: `CLMuatNamedCreds`
- UAT CLM download: `CLMuatDownload`
- S1 CLM API: `CLMs1NamedCreds`
- S1 CLM download: `CLMs1Download`
### eSignature
- eSignature REST: `Esignature_Demo_NamedCreds`
- OAuth/account server: `AcctDemo_NamedCreds`
## Current product behavior
### What is working now
- record-based CLM doc generation from Salesforce UI
- XML merge with repeating deficiencies
- account-based CLM config
- metadata-based letter-type selection with backward-compatible default behavior
- generated document status persistence on the case
- generated document attachment to the case as a Salesforce File
- account-based eSignature API exploration
- eSignature template listing
- eSignature envelope listing
### Known limitations
- no end-user workflow yet for creating or sending envelopes
- no template or envelope detail drill-down in the eSignature UI
- no persistent eSignature activity tracking on the case
- no dedicated admin UI yet for managing account configuration beyond Salesforce Setup
- some older docs still contain historical planning content
## Current template contract
### CLM XML merge fields
- `//AppraiserCaseNumber`
- `//AppraiserFieldReviewDate`
- `//PropertyAddress`
- `//PropertyStreet`
- `//PropertyCity`
- `//PropertyStateProvince`
- `//PropertyPostalCode`
- `//PropertyCountry`
### Deficiency repeating block
- row select: `//DeficiencyList/Deficiency`
- fields:
- `./Number`
- `./Description`
- `./Reference`
- `./Resolution`
## Near-term roadmap
- add eSignature detail calls for template detail and envelope detail
- add envelope creation and draft-send flows
- add better operator UX for large account lists and debugging output
- tighten documentation around setup and account configuration
- decide whether to retire `Property_Address__c` completely from metadata
## Out of scope for the current proof of concept
- full production-grade approval workflow
- end-user template authoring inside Salesforce
- large-scale reporting or analytics
- middleware outside Salesforce
---
Last updated: 2026-04-09

View File

@ -1,28 +1,26 @@
# Appraiser Review Letter Generator (Salesforce + DocuSign CLM) # Appraiser Review Letter Generator (Salesforce + DocuSign CLM)
## Welcome!
This project contains documentation and guides for building Appraiser Review Letter templates for use in Salesforce CLM (Contract Lifecycle Management).
---
## Overview ## Overview
This application automates generation of personalized appraiser review letters within Salesforce, merging Q&A responses and related data into a DocuSign CLM-powered letter. Content and layout adjust dynamically to each review scenario. This repo contains a Salesforce metadata project for generating appraiser review letters from `Appraiser_Case__c` data in DocuSign CLM, while also providing a growing proof-of-concept surface for DocuSign eSignature API workflows.
## Architecture Overview ## Current status
- Templates are rendered using dynamic data from Salesforce objects - The repo is standardized on the XML CLM merge path built around `AppraiserCasePayloadBuilder` + `CLMDocGenCallout`.
- All merge fields and arrays are mapped from Salesforce data model - `Appraiser_Case_Deficiency__c` is the canonical child deficiency object used by both the UI and document generation.
- Modular blocks for easy maintenance and expansion - Account-based DocuSign configuration lives in `CLM_Account_Setting__mdt`.
- The current Salesforce UI includes:
- the `Generate Review Letter` quick action for CLM doc gen
- the `Docusign eSignature` record tab for eSignature browsing
- CLM generation, task tracking, file attachment, and eSignature template/envelope browsing are all implemented.
## Key Features ## Read this first
- Dynamic merge of Appraiser Review answers (tables, paragraphs) - [PRODUCT_SPEC.md](./PRODUCT_SPEC.md): current product definition and architecture
- Salesforce-initiated CLM document generation and delivery - [CURRENT_STATUS.md](./CURRENT_STATUS.md): implementation snapshot and current recommendations
- Robust requirements, data, and design documentation - [SALESFORCE_SETUP.md](./SALESFORCE_SETUP.md): metadata and org setup notes
- [CLM_INTEGRATION.md](./CLM_INTEGRATION.md): CLM integration details
- [requirements.md](./requirements.md) and [design.md](./design.md): condensed companion docs aligned to the product spec
## Onboarding ## Documentation note
- Clone docs directory into your Salesforce project repo Older planning docs still exist, but `PRODUCT_SPEC.md`, `CURRENT_STATUS.md`, `SALESFORCE_SETUP.md`, and the source code are now the best reflection of the project.
- Review CLM_TEMPLATE_GUIDE.md for template logic and examples
- See requirements.md for merge field details
--- ---
_Last updated: 2026-02-26 13:23 PM_ _Last updated: 2026-04-09_
_Work in progress: Add quick-start workflow and test recommendations._

View File

@ -1,40 +1,72 @@
# Salesforce Setup — Appraiser Case + DocuSign CLM # Salesforce Setup — Appraiser Case + DocuSign CLM
> Status note (2026-04-07): the project is now standardized on `Appraiser_Case_Deficiency__c` for both UI display and CLM document generation.
## What was added ## What was added
### Custom object: `Appraiser_Case__c` ### Custom object: `Appraiser_Case__c`
- Auto-number name field labeled **Appraiser Case Number** - Auto-number name field labeled **Appraiser Case Number**
- `Appraiser_Field_Review_Date__c` (Date) - Dates: `Appraiser_Field_Review_Date__c`, `Letter_Sent_Date__c`
- `Property_Street__c` (Text 255) - Appraiser identity: `Appraiser_Name__c`, `Appraiser_Last_Name__c`, `Appraiser_Salutation__c`, `Appraiser_Email__c`
- `Property_City__c` (Text 80) - Appraiser address: `Appraiser_Street__c`, `Appraiser_City__c`, `Appraiser_State_Province__c`, `Appraiser_Postal_Code__c`, `Appraiser_Country__c`
- `Property_State_Province__c` (Text 80) - Property address: `Property_Street__c`, `Property_City__c`, `Property_State_Province__c`, `Property_Postal_Code__c`, `Property_Country__c`
- `Property_Postal_Code__c` (Text 20) - Case metadata: `FHA_Case_Number__c`
- `Property_Country__c` (Text 80) - CLM tracking: `Last_CLM_Account_Code__c`, `Last_DocGen_Status__c`, `Last_DocGen_Message__c`, `Last_DocGen_Task_Id__c`, `Last_DocGen_Task_Url__c`, `Last_Template_Document_Href__c`, `Last_Destination_Folder_Href__c`, `Last_DocGen_Requested_At__c`, `Last_DocGen_Completed_At__c`
- Generated document: `Generated_Document_Id__c`, `Generated_Document_Url__c`
- Attached file: `Attached_File_Content_Document_Id__c`, `Attached_File_Url__c`
### Child custom object: `Appraiser_Deficiency__c` ### Child custom object: `Appraiser_Case_Deficiency__c`
- Master-detail to `Appraiser_Case__c` - Master-detail to `Appraiser_Case__c`
- `Deficiency_Number__c` (Text 50) - `Deficiency_Number__c` (Number)
- `Description__c` (Long Text Area) - `Description__c` (Long Text Area)
- `Resolution__c` (Long Text Area) - `Resolution__c` (Long Text Area)
- `Sort_Order__c` (Number) - `Reference__c` (Text)
### Layouts ### Custom metadata types
- Basic page layout for Appraiser Case - `CLM_Account_Setting__mdt` — per-account CLM and eSignature configuration (Named Credentials, folder hrefs, template hrefs)
- Basic page layout for Appraiser Deficiency - `CLM_Letter_Definition__mdt` — per-account letter type definitions with optional folder/template overrides
- `CLM_Environment_Setting__mdt` — legacy environment defaults (UAT/S1); superseded by account settings
### Apex classes
- `AppraiserCasePayloadBuilder.cls` + test
- `CLMDocGenCallout.cls` + test
- `CLMAdminService.cls` + test
- `DocusignESignatureService.cls` + test
### Lightning Web Components
- `clmDocGenWorkbench` — CLM document generation UI (account selection, folder browsing, merge task submission, status polling, file attachment)
- `docusignEsignWorkbench` — eSignature API browsing (accounts, templates, envelopes)
- `clmRequestPreview` — merge request preview utility
### Named credentials
- `CLMuatNamedCreds`, `CLMs1NamedCreds` — CLM API calls
- `CLMuatDownload`, `CLMs1Download` — CLM document downloads
- `Esignature_Demo_NamedCreds` — eSignature REST API
- `AcctDemo_NamedCreds` — eSignature OAuth/userinfo (SecuredEndpoint backed by `DocusignJWT` external credential)
### Layouts and UI
- Page layouts for `Appraiser_Case__c` and `Appraiser_Case_Deficiency__c`
- Related list on Appraiser Case for deficiencies - Related list on Appraiser Case for deficiencies
- Basic list view on Appraiser Case - Record page: `Appraiser_Case_Record_Page`
- Quick action: `Generate Review Letter` (launches `clmDocGenWorkbench`)
### Tabs and permissions
- Custom tabs for both objects - Custom tabs for both objects
- Permission set: `Appraiser_Case_Admin` - Custom app: `Appraiser_Review`
### Apex ### Permissions
- `AppraiserCaseDocGenService.cls` - `Appraiser_Case_Admin` — full access
- `AppraiserCaseDocGenServiceTest.cls` - `Appraiser_Case_Access` — read/create access
### Sample data ### Sample data
- Anonymous Apex script: `scripts/apex/createSampleAppraiserCase.apex` - Anonymous Apex script: `scripts/apex/createSampleAppraiserCase.apex`
## Manual post-deploy steps
### DocusignJWT external credential
`force-app/main/default/externalCredentials/DocusignJWT.externalCredential-meta.xml` contains demo values for `iss` (integration key) and `sub` (user GUID) targeting `account-d.docusign.com`. Replace these with your org's actual integration key and impersonation user GUID before use.
---
## Deploy ## Deploy
From the project root: From the project root:
@ -52,7 +84,7 @@ sf project deploy start --source-dir /home/paulh/.openclaw/workspace/projects/sa
## Test Apex ## Test Apex
```bash ```bash
sf apex run test --tests AppraiserCaseDocGenServiceTest --result-format human sf apex run test --tests AppraiserCasePayloadBuilderTest --tests CLMAdminServiceTest --tests CLMDocGenCalloutTest --tests DocusignESignatureServiceTest --result-format human
``` ```
## Load sample data ## Load sample data
@ -73,12 +105,12 @@ sf org assign permset --name Appraiser_Case_Admin
2. Assign permission set. 2. Assign permission set.
3. Run the sample Apex script. 3. Run the sample Apex script.
4. Open the `Appraiser Case` tab and verify the record + related deficiencies. 4. Open the `Appraiser Case` tab and verify the record + related deficiencies.
5. Validate the JSON payload in debug logs or by running the Apex methods directly. 5. Validate the XML merge payload path in debug logs or by running the Apex methods directly.
6. Wire the DocuSign CLM launch path based on the exact package capability in your org. 6. Open an `Appraiser Case` record and use the `Generate Review Letter` action to browse folders/templates and submit a merge task.
## About the quick action ## About the quick action
A placeholder quick action metadata file was added to mark the launch point, but DocuSign CLM launch mechanics vary by package/org. See `docs/NEXT_STEPS_DOCGEN.md` for the practical wiring options. The `Generate Review Letter` quick action is now wired to the `clmDocGenWorkbench` LWC for interactive CLM browsing and merge submission.
## About “page layout” and “default setup” ## About “page layout” and “default setup”

4813
docs/appraiser-llm-chat.md Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,45 +1,48 @@
# Design — Appraiser Review Letter Generator # Design — Appraiser Review Letter Platform
## Architecture This document now serves as a compact architecture summary. The broader current-state reference is [PRODUCT_SPEC.md](/home/paulh/.openclaw/workspace/projects/salesforce-appraiser-review-letter/docs/PRODUCT_SPEC.md).
Describe the template structure, merge field handling logic, and integration with Salesforce CLM.
## Current architecture
### Salesforce records
- `Appraiser_Case__c` is the parent business object
- `Appraiser_Case_Deficiency__c` is the canonical child object
### Configuration
- `CLM_Account_Setting__mdt` stores account-level CLM and eSignature configuration
### CLM path
- [AppraiserCasePayloadBuilder.cls](/home/paulh/.openclaw/workspace/projects/salesforce-appraiser-review-letter/force-app/main/default/classes/AppraiserCasePayloadBuilder.cls)
- [CLMDocGenCallout.cls](/home/paulh/.openclaw/workspace/projects/salesforce-appraiser-review-letter/force-app/main/default/classes/CLMDocGenCallout.cls)
- [CLMAdminService.cls](/home/paulh/.openclaw/workspace/projects/salesforce-appraiser-review-letter/force-app/main/default/classes/CLMAdminService.cls)
- [clmDocGenWorkbench](/home/paulh/.openclaw/workspace/projects/salesforce-appraiser-review-letter/force-app/main/default/lwc/clmDocGenWorkbench/clmDocGenWorkbench.js)
### eSignature path
- [DocusignESignatureService.cls](/home/paulh/.openclaw/workspace/projects/salesforce-appraiser-review-letter/force-app/main/default/classes/DocusignESignatureService.cls)
- [docusignEsignWorkbench](/home/paulh/.openclaw/workspace/projects/salesforce-appraiser-review-letter/force-app/main/default/lwc/docusignEsignWorkbench/docusignEsignWorkbench.js)
## CLM merge design
- Salesforce builds XML merge data from the case and related deficiencies.
- CLM document generation uses `documentxmlmergetasks`.
- Folder and template selection are account-configurable and browsable in the UI.
- Task results and generated-document references are persisted back to `Appraiser_Case__c`.
- Generated CLM output can be downloaded via Named Credential and attached to the case as a Salesforce File.
## eSignature design
- Separate Named Credentials are used for REST calls and account-server OAuth/userinfo calls.
- The service currently supports:
- login information
- OAuth user info
- discovered accounts
- template listing
- envelope listing
- The current eSignature panel is an operator/admin browsing surface, not yet a business workflow.
## Design principles
- prefer account-based config over environment-only config
- keep DocuSign callouts in Apex behind UI-facing service methods
- persist important CLM results onto the business record
- use record-page or action-based LWCs for operator flows instead of Execute Anonymous
--- ---
Last updated: 2026-04-09
## Data Model
- Appraiser_Review__c: main record
- Appraiser_Review_Question__c: child (per Q&A)
## Merge Data (to CLM)
- Flat fields: appraiser, address, etc.
- Collection: reviewQuestions[] with Q, A, comments, etc.
## Template Structure
- Modular blocks (Header/Body/Footer)
- Dynamic sections for deficiencies, comments, summary
- Use of template language (Handlebars/Mustache/etc)
## Merge Logic
- Iterate lists (arrays) for tables
- Conditional display for sections/questions
- Null/empty handling (default text or suppression)
## Example Structure
```handlebars
{{#each DeficiencyList}}
<tr><td>{{DeficiencyType}}</td><td>{{DeficiencyDescription}}</td></tr>
{{/each}}
```
## CLM Template Guidance
- Repeat blocks/tables for reviewQuestions
- Conditional/variable blocks for rich, dynamic output
---
## Questions/Decisions
- Should merge logic be handled in Salesforce or intermediary middleware?
- What template engine is ideal for maintainability and troubleshooting?
---
_Last updated: 2026-02-26 13:20 PM_
_Work in progress: More complete architecture diagrams and template examples forthcoming._

View File

@ -1,47 +1,32 @@
# Requirements — Appraiser Review Letter Generator # Requirements — Appraiser Review Letter Platform
## Purpose This document now serves as a short requirements summary. The full current product specification is in [PRODUCT_SPEC.md](/home/paulh/.openclaw/workspace/projects/salesforce-appraiser-review-letter/docs/PRODUCT_SPEC.md).
Outline technical and functional requirements for Appraiser Review Letter templates in Salesforce CLM. Include assumed merge fields, integration points, edge cases, and design goals.
## Functional requirements
- A user can generate an appraiser review letter from an `Appraiser_Case__c` record in Salesforce.
- The generated CLM document merges property data and a repeating deficiency list from Salesforce.
- The user can choose a configured CLM account instead of hardcoding environment-specific values.
- The user can browse CLM templates and destination folders in the UI.
- The user can track CLM task status and attach the generated document back to the case.
- The system can browse core eSignature account data, templates, and envelopes for configured accounts.
## Non-functional requirements
- Account-specific configuration must be deployable and maintainable through Salesforce metadata.
- The solution must support both UAT and S1 CLM/eSignature account setups.
- The primary document-generation path must be test-covered in Apex.
- The solution should remain usable as a proof-of-concept platform for additional DocuSign API work.
## Canonical requirements decisions
- Canonical parent object: `Appraiser_Case__c`
- Canonical child deficiency object: `Appraiser_Case_Deficiency__c`
- Canonical CLM generation path: XML merge via `documentxmlmergetasks`
- Canonical config source: `CLM_Account_Setting__mdt`
- Structured property address fields are the source of truth; `Property_Address__c` is legacy
## Current open product questions
- What should the first production-grade eSignature workflow be: template detail, envelope detail, draft creation, or send?
- Should eSignature activity be persisted back onto `Appraiser_Case__c` the way CLM activity is?
- Should the eSignature workbench remain a testing/admin surface or evolve into a business-user workflow?
--- ---
Last updated: 2026-04-09
## Functional
- Reviewer completes form Q&A on Appraiser Review
- Each response drives tailored content in final letter (questions, comments, tables/blocks)
- Salesforce triggers CLM letter, merges data fields and Q&A collection
## Non-Functional
- Configurable (new Qs/fields easy to add)
- Audit and status tracking
- Support for complex/dynamic tables in output
## Key Requirements
- Support dynamic template merge fields (arrays/lists, booleans, enums)
- Render tables, paragraphs, and conditional sections
- Handle edge cases: nulls, empty lists, formatting gaps
- Provide fallback text for empty sections (e.g., 'No deficiencies found')
- Enable integration with Salesforce data model (objects: Appraisal, Deficiency, Reviewer)
## Example JSON
```json
{
"AppraisalId": "a1b2c3",
"DeficiencyList": [
{
"DeficiencyType": "Missing Docs",
"DeficiencyDescription": "Appraisal report lacking required documents."
}
],
"ReviewerComments": ["Well organized report."]
}
```
---
## Questions/Decisions
- What is the authoritative object and field schema for merge?
- Are custom field mappings needed for relationships (e.g. lookup fields)?
---
_Last updated: 2026-02-26 10:35 AM_
_Work in progress: Integration and schema expansion next._

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<CustomApplication xmlns="http://soap.sforce.com/2006/04/metadata">
<description>Dedicated Lightning app for Appraiser Case review and DocuSign CLM proof-of-concept testing.</description>
<formFactors>Small</formFactors>
<formFactors>Large</formFactors>
<isNavAutoTempTabsDisabled>false</isNavAutoTempTabsDisabled>
<isNavPersonalizationDisabled>false</isNavPersonalizationDisabled>
<isNavTabPersistenceDisabled>false</isNavTabPersistenceDisabled>
<isOmniPinnedViewEnabled>false</isOmniPinnedViewEnabled>
<label>Appraiser Review</label>
<navType>Standard</navType>
<tabs>standard-home</tabs>
<tabs>Appraiser_Case__c</tabs>
<tabs>Appraiser_Case_Deficiency__c</tabs>
<tabs>standard-report</tabs>
<tabs>standard-Dashboard</tabs>
<uiType>Lightning</uiType>
</CustomApplication>

View File

@ -1,103 +0,0 @@
public with sharing class AppraiserCaseDocGenService {
public class DeficiencyDTO {
@AuraEnabled public String deficiencyNumber;
@AuraEnabled public String description;
@AuraEnabled public String resolution;
@AuraEnabled public Decimal sortOrder;
}
public class AppraiserCasePayload {
@AuraEnabled public Id caseId;
@AuraEnabled public String appraiserCaseNumber;
@AuraEnabled public Date appraiserFieldReviewDate;
@AuraEnabled public String propertyStreet;
@AuraEnabled public String propertyCity;
@AuraEnabled public String propertyStateProvince;
@AuraEnabled public String propertyPostalCode;
@AuraEnabled public String propertyCountry;
@AuraEnabled public String propertyAddressSingleLine;
@AuraEnabled public List<DeficiencyDTO> deficiencies;
}
@AuraEnabled(cacheable=false)
public static String buildPayloadJson(Id appraiserCaseId) {
return JSON.serialize(buildPayload(appraiserCaseId));
}
public static AppraiserCasePayload buildPayload(Id appraiserCaseId) {
Appraiser_Case__c appraiserCase = [
SELECT Id,
Name,
Appraiser_Field_Review_Date__c,
Property_Street__c,
Property_City__c,
Property_State_Province__c,
Property_Postal_Code__c,
Property_Country__c,
(SELECT Id,
Name,
Deficiency_Number__c,
Description__c,
Resolution__c,
Sort_Order__c
FROM Appraiser_Deficiencies__r
ORDER BY Sort_Order__c ASC, CreatedDate ASC)
FROM Appraiser_Case__c
WHERE Id = :appraiserCaseId
LIMIT 1
];
AppraiserCasePayload payload = new AppraiserCasePayload();
payload.caseId = appraiserCase.Id;
payload.appraiserCaseNumber = appraiserCase.Name;
payload.appraiserFieldReviewDate = appraiserCase.Appraiser_Field_Review_Date__c;
payload.propertyStreet = appraiserCase.Property_Street__c;
payload.propertyCity = appraiserCase.Property_City__c;
payload.propertyStateProvince = appraiserCase.Property_State_Province__c;
payload.propertyPostalCode = appraiserCase.Property_Postal_Code__c;
payload.propertyCountry = appraiserCase.Property_Country__c;
payload.propertyAddressSingleLine = buildAddress(appraiserCase);
payload.deficiencies = new List<DeficiencyDTO>();
for (Appraiser_Deficiency__c deficiency : appraiserCase.Appraiser_Deficiencies__r) {
DeficiencyDTO dto = new DeficiencyDTO();
dto.deficiencyNumber = deficiency.Deficiency_Number__c;
dto.description = deficiency.Description__c;
dto.resolution = deficiency.Resolution__c;
dto.sortOrder = deficiency.Sort_Order__c;
payload.deficiencies.add(dto);
}
return payload;
}
private static String buildAddress(Appraiser_Case__c appraiserCase) {
List<String> parts = new List<String>();
if (String.isNotBlank(appraiserCase.Property_Street__c)) parts.add(appraiserCase.Property_Street__c);
List<String> cityLine = new List<String>();
if (String.isNotBlank(appraiserCase.Property_City__c)) cityLine.add(appraiserCase.Property_City__c);
if (String.isNotBlank(appraiserCase.Property_State_Province__c)) cityLine.add(appraiserCase.Property_State_Province__c);
if (String.isNotBlank(appraiserCase.Property_Postal_Code__c)) cityLine.add(appraiserCase.Property_Postal_Code__c);
if (!cityLine.isEmpty()) parts.add(String.join(cityLine, ', '));
if (String.isNotBlank(appraiserCase.Property_Country__c)) parts.add(appraiserCase.Property_Country__c);
return String.join(parts, ' | ');
}
@AuraEnabled(cacheable=false)
public static Map<String, Object> buildDocGenRequest(Id appraiserCaseId, String templateKey) {
AppraiserCasePayload payload = buildPayload(appraiserCaseId);
Map<String, Object> requestBody = new Map<String, Object>();
requestBody.put('templateKey', templateKey);
requestBody.put('recordId', appraiserCaseId);
requestBody.put('sourceObject', 'Appraiser_Case__c');
requestBody.put('mergeData', payload);
return requestBody;
}
@AuraEnabled(cacheable=false)
public static String buildDocGenRequestJson(Id appraiserCaseId, String templateKey) {
return JSON.serialize(buildDocGenRequest(appraiserCaseId, templateKey));
}
}

View File

@ -1,36 +0,0 @@
@IsTest
private class AppraiserCaseDocGenServiceTest {
@IsTest
static void buildsPayloadAndRequestJson() {
Appraiser_Case__c appraiserCase = new Appraiser_Case__c(
Appraiser_Field_Review_Date__c = Date.newInstance(2026, 4, 1),
Property_Street__c = '123 Main St',
Property_City__c = 'Ottawa',
Property_State_Province__c = 'ON',
Property_Postal_Code__c = 'K1A 0A1',
Property_Country__c = 'Canada'
);
insert appraiserCase;
insert new Appraiser_Deficiency__c(
Name = 'Deficiency 1',
Appraiser_Case__c = appraiserCase.Id,
Deficiency_Number__c = '1',
Description__c = 'Missing comparable sale analysis',
Resolution__c = 'Provide updated comparable sales section',
Sort_Order__c = 1
);
Test.startTest();
AppraiserCaseDocGenService.AppraiserCasePayload payload = AppraiserCaseDocGenService.buildPayload(appraiserCase.Id);
String json = AppraiserCaseDocGenService.buildDocGenRequestJson(appraiserCase.Id, 'APPRAISER_REVIEW_LETTER');
Test.stopTest();
System.assertEquals(appraiserCase.Id, payload.caseId);
System.assertEquals('Ottawa', payload.propertyCity);
System.assertEquals(1, payload.deficiencies.size());
System.assertEquals('1', payload.deficiencies[0].deficiencyNumber);
System.assert(json.contains('APPRAISER_REVIEW_LETTER'));
System.assert(json.contains('Missing comparable sale analysis'));
}
}

View File

@ -21,7 +21,30 @@ public class AppraiserCasePayloadBuilder {
Map<String, Object> payload = new Map<String, Object>(); Map<String, Object> payload = new Map<String, Object>();
payload.put('AppraiserCaseNumber', appraiserCase.Name); payload.put('AppraiserCaseNumber', appraiserCase.Name);
payload.put('AppraiserFieldReviewDate', formatDate(appraiserCase.Appraiser_Field_Review_Date__c)); payload.put('AppraiserFieldReviewDate', formatDate(appraiserCase.Appraiser_Field_Review_Date__c));
payload.put('PropertyAddress', appraiserCase.Property_Address__c); payload.put('LetterSentDate', formatDate(appraiserCase.Letter_Sent_Date__c));
payload.put('FHACaseNumber', appraiserCase.FHA_Case_Number__c);
payload.put('AppraiserName', appraiserCase.Appraiser_Name__c);
payload.put('AppraiserSalutation', appraiserCase.Appraiser_Salutation__c);
payload.put('AppraiserLastName', appraiserCase.Appraiser_Last_Name__c);
payload.put('AppraiserEmail', appraiserCase.Appraiser_Email__c);
payload.put('AppraiserStreet', appraiserCase.Appraiser_Street__c);
payload.put('AppraiserCity', appraiserCase.Appraiser_City__c);
payload.put('AppraiserStateProvince', appraiserCase.Appraiser_State_Province__c);
payload.put('AppraiserPostalCode', appraiserCase.Appraiser_Postal_Code__c);
payload.put('AppraiserCountry', appraiserCase.Appraiser_Country__c);
payload.put('AppraiserAddress', formatMailingAddress(
appraiserCase.Appraiser_Street__c,
appraiserCase.Appraiser_City__c,
appraiserCase.Appraiser_State_Province__c,
appraiserCase.Appraiser_Postal_Code__c,
appraiserCase.Appraiser_Country__c
));
payload.put('PropertyStreet', appraiserCase.Property_Street__c);
payload.put('PropertyCity', appraiserCase.Property_City__c);
payload.put('PropertyStateProvince', appraiserCase.Property_State_Province__c);
payload.put('PropertyPostalCode', appraiserCase.Property_Postal_Code__c);
payload.put('PropertyCountry', appraiserCase.Property_Country__c);
payload.put('PropertyAddress', formatAddress(appraiserCase));
// Transform child deficiencies into DeficiencyList array // Transform child deficiencies into DeficiencyList array
List<Map<String, Object>> deficiencyList = new List<Map<String, Object>>(); List<Map<String, Object>> deficiencyList = new List<Map<String, Object>>();
@ -31,6 +54,7 @@ public class AppraiserCasePayloadBuilder {
defMap.put('deficiencyNumber', deficiency.Deficiency_Number__c); defMap.put('deficiencyNumber', deficiency.Deficiency_Number__c);
defMap.put('description', deficiency.Description__c); defMap.put('description', deficiency.Description__c);
defMap.put('resolution', deficiency.Resolution__c); defMap.put('resolution', deficiency.Resolution__c);
defMap.put('reference', deficiency.Reference__c);
deficiencyList.add(defMap); deficiencyList.add(defMap);
} }
} }
@ -60,8 +84,23 @@ public class AppraiserCasePayloadBuilder {
Id, Id,
Name, Name,
Appraiser_Field_Review_Date__c, Appraiser_Field_Review_Date__c,
Property_Address__c, Letter_Sent_Date__c,
(SELECT Id, Deficiency_Number__c, Description__c, Resolution__c FHA_Case_Number__c,
Appraiser_Name__c,
Appraiser_Salutation__c,
Appraiser_Last_Name__c,
Appraiser_Email__c,
Appraiser_Street__c,
Appraiser_City__c,
Appraiser_State_Province__c,
Appraiser_Postal_Code__c,
Appraiser_Country__c,
Property_Street__c,
Property_City__c,
Property_State_Province__c,
Property_Postal_Code__c,
Property_Country__c,
(SELECT Id, Deficiency_Number__c, Description__c, Resolution__c, Reference__c
FROM Deficiencies__r FROM Deficiencies__r
ORDER BY Deficiency_Number__c ASC) ORDER BY Deficiency_Number__c ASC)
FROM Appraiser_Case__c FROM Appraiser_Case__c
@ -73,11 +112,60 @@ public class AppraiserCasePayloadBuilder {
} }
/** /**
* @description Format date for CLM merge (YYYY-MM-DD or null). * @description Format date for CLM merge (for example Apr 09, 2026) or null.
* @param dt Date field value * @param dt Date field value
* @return String Formatted date or null * @return String Formatted date or null
*/ */
private static String formatDate(Date dt) { private static String formatDate(Date dt) {
return dt != null ? dt.format() : null; return dt != null
? DateTime.newInstance(dt, Time.newInstance(0, 0, 0, 0)).format('MMM dd, yyyy')
: null;
}
private static String formatAddress(Appraiser_Case__c appraiserCase) {
return formatMailingAddress(
appraiserCase.Property_Street__c,
appraiserCase.Property_City__c,
appraiserCase.Property_State_Province__c,
appraiserCase.Property_Postal_Code__c,
appraiserCase.Property_Country__c
);
}
public static String formatMailingAddress(
String street,
String city,
String stateProvince,
String postalCode,
String country
) {
List<String> lines = new List<String>();
if (String.isNotBlank(street)) {
lines.add(street.trim());
}
List<String> localityParts = new List<String>();
if (String.isNotBlank(city)) {
localityParts.add(city.trim());
}
if (String.isNotBlank(stateProvince)) {
localityParts.add(stateProvince.trim());
}
String locality = String.join(localityParts, ', ');
if (String.isNotBlank(postalCode)) {
locality = String.isNotBlank(locality)
? locality + ' ' + postalCode.trim()
: postalCode.trim();
}
if (String.isNotBlank(locality)) {
lines.add(locality);
}
if (String.isNotBlank(country)) {
lines.add(country.trim());
}
return lines.isEmpty() ? null : String.join(lines, ', ');
} }
} }

View File

@ -5,8 +5,23 @@ private class AppraiserCasePayloadBuilderTest {
static void setupTestData() { static void setupTestData() {
// Create test Appraiser Case // Create test Appraiser Case
Appraiser_Case__c testCase = new Appraiser_Case__c( Appraiser_Case__c testCase = new Appraiser_Case__c(
Appraiser_Field_Review_Date__c = Date.parse('04/02/2026'), Appraiser_Field_Review_Date__c = Date.newInstance(2026, 4, 2),
Property_Address__c = '123 Main St, Denver, CO 80202' Letter_Sent_Date__c = Date.newInstance(2026, 4, 9),
FHA_Case_Number__c = '123-4567890',
Appraiser_Name__c = 'Jamie Appraiser',
Appraiser_Salutation__c = 'Ms.',
Appraiser_Last_Name__c = 'Appraiser',
Appraiser_Email__c = 'jamie.appraiser@example.com',
Appraiser_Street__c = '245 Lexington Ave',
Appraiser_City__c = 'New York',
Appraiser_State_Province__c = 'NY',
Appraiser_Postal_Code__c = '10016',
Appraiser_Country__c = 'USA',
Property_Street__c = '123 Main St',
Property_City__c = 'Denver',
Property_State_Province__c = 'CO',
Property_Postal_Code__c = '80202',
Property_Country__c = 'USA'
); );
insert testCase; insert testCase;
@ -16,13 +31,15 @@ private class AppraiserCasePayloadBuilderTest {
Appraiser_Case__c = testCase.Id, Appraiser_Case__c = testCase.Id,
Deficiency_Number__c = 1, Deficiency_Number__c = 1,
Description__c = 'Missing comparable sale adjustment detail.', Description__c = 'Missing comparable sale adjustment detail.',
Resolution__c = 'Added adjustment rationale and supporting calculations.' Resolution__c = 'Added adjustment rationale and supporting calculations.',
Reference__c = 'VC-1'
)); ));
testDefs.add(new Appraiser_Case_Deficiency__c( testDefs.add(new Appraiser_Case_Deficiency__c(
Appraiser_Case__c = testCase.Id, Appraiser_Case__c = testCase.Id,
Deficiency_Number__c = 2, Deficiency_Number__c = 2,
Description__c = 'Neighborhood trend explanation insufficient.', Description__c = 'Neighborhood trend explanation insufficient.',
Resolution__c = 'Expanded market trend narrative with MLS evidence.' Resolution__c = 'Expanded market trend narrative with MLS evidence.',
Reference__c = 'MC-2'
)); ));
insert testDefs; insert testDefs;
} }
@ -36,11 +53,39 @@ private class AppraiserCasePayloadBuilderTest {
Assert.isNotNull(payload, 'Payload should not be null'); Assert.isNotNull(payload, 'Payload should not be null');
Assert.isTrue(payload.containsKey('AppraiserCaseNumber'), 'Payload should contain AppraiserCaseNumber'); Assert.isTrue(payload.containsKey('AppraiserCaseNumber'), 'Payload should contain AppraiserCaseNumber');
Assert.isTrue(payload.containsKey('AppraiserFieldReviewDate'), 'Payload should contain AppraiserFieldReviewDate'); Assert.isTrue(payload.containsKey('AppraiserFieldReviewDate'), 'Payload should contain AppraiserFieldReviewDate');
Assert.isTrue(payload.containsKey('LetterSentDate'), 'Payload should contain LetterSentDate');
Assert.isTrue(payload.containsKey('FHACaseNumber'), 'Payload should contain FHACaseNumber');
Assert.isTrue(payload.containsKey('AppraiserName'), 'Payload should contain AppraiserName');
Assert.isTrue(payload.containsKey('AppraiserSalutation'), 'Payload should contain AppraiserSalutation');
Assert.isTrue(payload.containsKey('AppraiserLastName'), 'Payload should contain AppraiserLastName');
Assert.isTrue(payload.containsKey('AppraiserEmail'), 'Payload should contain AppraiserEmail');
Assert.isTrue(payload.containsKey('AppraiserAddress'), 'Payload should contain AppraiserAddress');
Assert.isTrue(payload.containsKey('PropertyAddress'), 'Payload should contain PropertyAddress'); Assert.isTrue(payload.containsKey('PropertyAddress'), 'Payload should contain PropertyAddress');
Assert.isTrue(payload.containsKey('PropertyStreet'), 'Payload should contain PropertyStreet');
Assert.isTrue(payload.containsKey('PropertyCity'), 'Payload should contain PropertyCity');
Assert.isTrue(payload.containsKey('PropertyStateProvince'), 'Payload should contain PropertyStateProvince');
Assert.isTrue(payload.containsKey('PropertyPostalCode'), 'Payload should contain PropertyPostalCode');
Assert.isTrue(payload.containsKey('PropertyCountry'), 'Payload should contain PropertyCountry');
Assert.isTrue(payload.containsKey('DeficiencyList'), 'Payload should contain DeficiencyList'); Assert.isTrue(payload.containsKey('DeficiencyList'), 'Payload should contain DeficiencyList');
Assert.areEqual('123 Main St', (String) payload.get('PropertyStreet'));
Assert.areEqual('Denver', (String) payload.get('PropertyCity'));
Assert.areEqual('CO', (String) payload.get('PropertyStateProvince'));
Assert.areEqual('80202', (String) payload.get('PropertyPostalCode'));
Assert.areEqual('USA', (String) payload.get('PropertyCountry'));
Assert.areEqual('123-4567890', (String) payload.get('FHACaseNumber'));
Assert.areEqual('Apr 02, 2026', (String) payload.get('AppraiserFieldReviewDate'));
Assert.areEqual('Apr 09, 2026', (String) payload.get('LetterSentDate'));
Assert.areEqual('Jamie Appraiser', (String) payload.get('AppraiserName'));
Assert.areEqual('Ms.', (String) payload.get('AppraiserSalutation'));
Assert.areEqual('Appraiser', (String) payload.get('AppraiserLastName'));
Assert.areEqual('jamie.appraiser@example.com', (String) payload.get('AppraiserEmail'));
Assert.areEqual('245 Lexington Ave, New York, NY 10016, USA', (String) payload.get('AppraiserAddress'));
Assert.areEqual('123 Main St, Denver, CO 80202, USA', (String) payload.get('PropertyAddress'));
List<Object> deficiencyList = (List<Object>) payload.get('DeficiencyList'); List<Object> deficiencyList = (List<Object>) payload.get('DeficiencyList');
Assert.areEqual(2, deficiencyList.size(), 'DeficiencyList should contain 2 items'); Assert.areEqual(2, deficiencyList.size(), 'DeficiencyList should contain 2 items');
Map<String, Object> firstDeficiency = (Map<String, Object>) deficiencyList[0];
Assert.areEqual('VC-1', (String) firstDeficiency.get('reference'));
} }
@IsTest @IsTest
@ -58,7 +103,10 @@ private class AppraiserCasePayloadBuilderTest {
static void testPayloadWithNullDate() { static void testPayloadWithNullDate() {
// Create case without review date // Create case without review date
Appraiser_Case__c testCase = new Appraiser_Case__c( Appraiser_Case__c testCase = new Appraiser_Case__c(
Property_Address__c = '456 Oak Ave, Boulder, CO 80301' Property_Street__c = '456 Oak Ave',
Property_City__c = 'Boulder',
Property_State_Province__c = 'CO',
Property_Postal_Code__c = '80301'
); );
insert testCase; insert testCase;
@ -66,6 +114,7 @@ private class AppraiserCasePayloadBuilderTest {
Assert.isNotNull(payload, 'Payload should not be null even with null date'); Assert.isNotNull(payload, 'Payload should not be null even with null date');
Assert.isNull(payload.get('AppraiserFieldReviewDate'), 'Null date should map to null in payload'); Assert.isNull(payload.get('AppraiserFieldReviewDate'), 'Null date should map to null in payload');
Assert.areEqual('456 Oak Ave, Boulder, CO 80301', (String) payload.get('PropertyAddress'));
} }
@IsTest @IsTest

View File

@ -0,0 +1,744 @@
public with sharing class CLMAdminService {
public class CaseDeficiencyItem {
@AuraEnabled public Id recordId;
@AuraEnabled public Decimal deficiencyNumber;
@AuraEnabled public String description;
@AuraEnabled public String resolution;
@AuraEnabled public String reference;
}
public class CaseContext {
@AuraEnabled public Id caseId;
@AuraEnabled public String caseNumber;
@AuraEnabled public String propertyAddress;
@AuraEnabled public String lastDocGenStatus;
@AuraEnabled public String lastDocGenMessage;
@AuraEnabled public String lastClmAccountCode;
@AuraEnabled public String lastDocGenTaskId;
@AuraEnabled public String lastDocGenTaskUrl;
@AuraEnabled public String generatedDocumentUrl;
@AuraEnabled public String generatedDocumentId;
@AuraEnabled public String attachedFileContentDocumentId;
@AuraEnabled public String attachedFileUrl;
@AuraEnabled public Datetime lastDocGenRequestedAt;
@AuraEnabled public Datetime lastDocGenCompletedAt;
@AuraEnabled public List<CaseDeficiencyItem> deficiencies;
}
public class FileAttachmentResult {
@AuraEnabled public Boolean success;
@AuraEnabled public String message;
@AuraEnabled public String contentDocumentId;
@AuraEnabled public String fileUrl;
@AuraEnabled public String fileTitle;
}
public class AccountSettings {
@AuraEnabled public String accountCode;
@AuraEnabled public String accountDisplayName;
@AuraEnabled public String environment;
@AuraEnabled public String clmAccountId;
@AuraEnabled public String clmApiNamedCredential;
@AuraEnabled public String clmDownloadNamedCredential;
@AuraEnabled public String eSignatureRestNamedCredential;
@AuraEnabled public String templateRootFolderHref;
@AuraEnabled public String destinationRootFolderHref;
@AuraEnabled public String defaultTemplateDocumentHref;
@AuraEnabled public String defaultDocumentNamePrefix;
@AuraEnabled public Boolean active;
}
public class LetterSettings {
@AuraEnabled public String accountCode;
@AuraEnabled public String letterCode;
@AuraEnabled public String letterDisplayName;
@AuraEnabled public String description;
@AuraEnabled public Boolean isDefault;
@AuraEnabled public Decimal sortOrder;
@AuraEnabled public String templateRootFolderHref;
@AuraEnabled public String destinationRootFolderHref;
@AuraEnabled public String defaultTemplateDocumentHref;
@AuraEnabled public String defaultDocumentNamePrefix;
@AuraEnabled public Boolean active;
}
public class ResourceItem {
@AuraEnabled public String name;
@AuraEnabled public String href;
@AuraEnabled public String type;
@AuraEnabled public String parentHref;
@AuraEnabled public String rawJson;
}
public class FolderContents {
@AuraEnabled public ResourceItem folder;
@AuraEnabled public List<ResourceItem> folders;
@AuraEnabled public List<ResourceItem> documents;
}
public class DocGenPreview {
@AuraEnabled public String accountCode;
@AuraEnabled public String accountDisplayName;
@AuraEnabled public String letterCode;
@AuraEnabled public String letterDisplayName;
@AuraEnabled public String mergeTaskEndpointUrl;
@AuraEnabled public String templateDocHref;
@AuraEnabled public String destinationFolderHref;
@AuraEnabled public String destinationDocName;
@AuraEnabled public String payloadJson;
@AuraEnabled public String dataXml;
@AuraEnabled public String requestBodyJson;
}
@AuraEnabled(cacheable=true)
public static List<AccountSettings> listAccountSettings() {
List<AccountSettings> settings = new List<AccountSettings>();
for (CLM_Account_Setting__mdt row : [
SELECT DeveloperName,
Account_Code__c,
Account_Display_Name__c,
Environment_Code__c,
CLM_Account_Id__c,
CLM_Api_Named_Credential__c,
CLM_Download_Named_Credential__c,
ESignature_Rest_Named_Credential__c,
Template_Root_Folder_Href__c,
Destination_Root_Folder_Href__c,
Default_Template_Document_Href__c,
Default_Destination_Document_Name_Prefix__c,
Active__c
FROM CLM_Account_Setting__mdt
WHERE Active__c = true
ORDER BY Account_Display_Name__c ASC, DeveloperName ASC
]) {
settings.add(toAccountSettings(row));
}
return settings;
}
@AuraEnabled(cacheable=true)
public static AccountSettings getAccountSettings(String accountCode) {
CLM_Account_Setting__mdt row = resolveAccountSetting(accountCode);
return row == null ? null : toAccountSettings(row);
}
@AuraEnabled(cacheable=true)
public static List<LetterSettings> listLetterSettings(String accountCode) {
AccountSettings account = getAccountSettings(accountCode);
List<LetterSettings> letters = new List<LetterSettings>();
if (account == null) {
return letters;
}
for (CLM_Letter_Definition__mdt row : [
SELECT DeveloperName,
Account_Code__c,
Letter_Code__c,
Letter_Display_Name__c,
Description__c,
Active__c,
Is_Default__c,
Sort_Order__c,
Template_Root_Folder_Href__c,
Destination_Root_Folder_Href__c,
Default_Template_Document_Href__c,
Default_Destination_Document_Name_Prefix__c
FROM CLM_Letter_Definition__mdt
WHERE Active__c = true
AND Account_Code__c = :account.accountCode
ORDER BY Is_Default__c DESC, Sort_Order__c ASC, Letter_Display_Name__c ASC, DeveloperName ASC
]) {
letters.add(toLetterSettings(row, account));
}
if (letters.isEmpty()) {
letters.add(buildFallbackLetterSettings(account));
}
return letters;
}
@AuraEnabled(cacheable=true)
public static LetterSettings getLetterSettings(String accountCode, String letterCode) {
AccountSettings account = getAccountSettings(accountCode);
if (account == null) {
return null;
}
String normalizedLetterCode = String.isBlank(letterCode) ? null : letterCode.trim();
if (String.isNotBlank(normalizedLetterCode)) {
List<CLM_Letter_Definition__mdt> rows = [
SELECT DeveloperName,
Account_Code__c,
Letter_Code__c,
Letter_Display_Name__c,
Description__c,
Active__c,
Is_Default__c,
Sort_Order__c,
Template_Root_Folder_Href__c,
Destination_Root_Folder_Href__c,
Default_Template_Document_Href__c,
Default_Destination_Document_Name_Prefix__c
FROM CLM_Letter_Definition__mdt
WHERE Active__c = true
AND Account_Code__c = :account.accountCode
AND Letter_Code__c = :normalizedLetterCode
LIMIT 1
];
if (!rows.isEmpty()) {
return toLetterSettings(rows[0], account);
}
}
List<LetterSettings> letters = listLetterSettings(account.accountCode);
for (LetterSettings letter : letters) {
if (letter.isDefault) {
return letter;
}
}
return letters.isEmpty() ? buildFallbackLetterSettings(account) : letters[0];
}
@AuraEnabled(cacheable=false)
public static CLMDocGenCallout.CLMDocGenResponse generateDocument(
Id appraiserCaseId,
String templateDocHref,
String destinationFolderHref,
String destinationDocName,
String accountCode
) {
CLM_Account_Setting__mdt account = requireAccountSetting(accountCode);
CLMDocGenCallout.CLMDocGenResponse response = CLMDocGenCallout.generateDocument(
(String) appraiserCaseId,
templateDocHref,
destinationFolderHref,
destinationDocName,
account.Environment_Code__c,
account.CLM_Account_Id__c,
account.CLM_Api_Named_Credential__c
);
persistDocGenResult(appraiserCaseId, templateDocHref, destinationFolderHref, response, false, accountCode);
return response;
}
@AuraEnabled(cacheable=false)
public static CLMDocGenCallout.CLMDocGenResponse getTaskStatus(Id appraiserCaseId, String taskId, String accountCode) {
CLM_Account_Setting__mdt account = requireAccountSetting(accountCode);
CLMDocGenCallout.CLMDocGenResponse response = CLMDocGenCallout.getTaskStatus(
taskId,
account.Environment_Code__c,
account.CLM_Account_Id__c,
account.CLM_Api_Named_Credential__c
);
persistDocGenResult(appraiserCaseId, null, null, response, true, accountCode);
return response;
}
@AuraEnabled(cacheable=false)
public static String probeResource(String resourceOrHref, String accountCode) {
CLM_Account_Setting__mdt account = requireAccountSetting(accountCode);
return performGet(resourceOrHref, account).getBody();
}
@AuraEnabled(cacheable=false)
public static DocGenPreview getDocGenPreview(Id appraiserCaseId, String accountCode, String letterCode) {
if (appraiserCaseId == null) {
throw new AuraHandledException('appraiserCaseId is required');
}
AccountSettings account = getAccountSettings(accountCode);
if (account == null) {
throw new AuraHandledException('No active CLM account setting was found for ' + accountCode + '.');
}
LetterSettings letter = getLetterSettings(account.accountCode, letterCode);
List<Appraiser_Case__c> previewRows = [
SELECT Id, Name
FROM Appraiser_Case__c
WHERE Id = :appraiserCaseId
LIMIT 1
];
if (previewRows.isEmpty()) {
throw new AuraHandledException('Appraiser Case not found: ' + appraiserCaseId);
}
Appraiser_Case__c appraiserCase = previewRows[0];
String prefix = letter != null && String.isNotBlank(letter.defaultDocumentNamePrefix)
? letter.defaultDocumentNamePrefix
: account.defaultDocumentNamePrefix;
DocGenPreview preview = new DocGenPreview();
preview.accountCode = account.accountCode;
preview.accountDisplayName = account.accountDisplayName;
preview.letterCode = letter != null ? letter.letterCode : 'APPRAISER_REVIEW';
preview.letterDisplayName = letter != null ? letter.letterDisplayName : 'Appraiser Review Letter';
preview.templateDocHref = letter != null ? letter.defaultTemplateDocumentHref : account.defaultTemplateDocumentHref;
preview.destinationFolderHref = letter != null ? letter.destinationRootFolderHref : account.destinationRootFolderHref;
preview.destinationDocName = buildDefaultDocumentName(prefix, appraiserCase.Name);
preview.mergeTaskEndpointUrl = CLMDocGenCallout.buildDocumentXmlMergeTasksUrl(
preview.templateDocHref,
preview.destinationFolderHref,
account.environment,
account.clmAccountId
);
preview.payloadJson = JSON.serializePretty(AppraiserCasePayloadBuilder.buildPayload((String) appraiserCaseId));
preview.dataXml = CLMDocGenCallout.prettyPrintXml(CLMDocGenCallout.buildDataXmlForCase((String) appraiserCaseId));
preview.requestBodyJson = CLMDocGenCallout.buildRequestBodyJson(
(String) appraiserCaseId,
preview.templateDocHref,
preview.destinationFolderHref,
preview.destinationDocName
);
return preview;
}
@AuraEnabled(cacheable=false)
public static CaseContext getCaseContext(Id appraiserCaseId) {
List<Appraiser_Case__c> contextRows = [
SELECT Id,
Name,
Property_Street__c,
Property_City__c,
Property_State_Province__c,
Property_Postal_Code__c,
Property_Country__c,
Last_DocGen_Status__c,
Last_DocGen_Message__c,
Last_CLM_Account_Code__c,
Last_DocGen_Task_Id__c,
Last_DocGen_Task_Url__c,
Generated_Document_Url__c,
Generated_Document_Id__c,
Attached_File_Content_Document_Id__c,
Attached_File_Url__c,
Last_DocGen_Requested_At__c,
Last_DocGen_Completed_At__c,
(SELECT Id,
Deficiency_Number__c,
Description__c,
Resolution__c,
Reference__c
FROM Deficiencies__r
ORDER BY Deficiency_Number__c ASC, CreatedDate ASC)
FROM Appraiser_Case__c
WHERE Id = :appraiserCaseId
LIMIT 1
];
if (contextRows.isEmpty()) {
throw new AuraHandledException('Appraiser Case not found: ' + appraiserCaseId);
}
Appraiser_Case__c appraiserCase = contextRows[0];
CaseContext context = new CaseContext();
context.caseId = appraiserCase.Id;
context.caseNumber = appraiserCase.Name;
context.propertyAddress = formatAddress(appraiserCase);
context.lastDocGenStatus = appraiserCase.Last_DocGen_Status__c;
context.lastDocGenMessage = appraiserCase.Last_DocGen_Message__c;
context.lastClmAccountCode = appraiserCase.Last_CLM_Account_Code__c;
context.lastDocGenTaskId = appraiserCase.Last_DocGen_Task_Id__c;
context.lastDocGenTaskUrl = appraiserCase.Last_DocGen_Task_Url__c;
context.generatedDocumentUrl = appraiserCase.Generated_Document_Url__c;
context.generatedDocumentId = appraiserCase.Generated_Document_Id__c;
context.attachedFileContentDocumentId = appraiserCase.Attached_File_Content_Document_Id__c;
context.attachedFileUrl = appraiserCase.Attached_File_Url__c;
context.lastDocGenRequestedAt = appraiserCase.Last_DocGen_Requested_At__c;
context.lastDocGenCompletedAt = appraiserCase.Last_DocGen_Completed_At__c;
context.deficiencies = new List<CaseDeficiencyItem>();
if (appraiserCase.Deficiencies__r != null) {
for (Appraiser_Case_Deficiency__c deficiency : appraiserCase.Deficiencies__r) {
CaseDeficiencyItem item = new CaseDeficiencyItem();
item.recordId = deficiency.Id;
item.deficiencyNumber = deficiency.Deficiency_Number__c;
item.description = deficiency.Description__c;
item.resolution = deficiency.Resolution__c;
item.reference = deficiency.Reference__c;
context.deficiencies.add(item);
}
}
return context;
}
@AuraEnabled(cacheable=false)
public static FileAttachmentResult attachGeneratedDocumentToCase(Id appraiserCaseId, String accountCode) {
if (appraiserCaseId == null) {
throw new AuraHandledException('appraiserCaseId is required');
}
CLM_Account_Setting__mdt account = requireAccountSetting(accountCode);
Appraiser_Case__c appraiserCase = [
SELECT Id,
Name,
Generated_Document_Url__c,
Generated_Document_Id__c
FROM Appraiser_Case__c
WHERE Id = :appraiserCaseId
LIMIT 1
];
if (String.isBlank(appraiserCase.Generated_Document_Url__c)) {
throw new AuraHandledException('No generated document is available to attach yet.');
}
CLMDocGenCallout.DownloadedDocument downloaded = CLMDocGenCallout.downloadDocument(
appraiserCase.Generated_Document_Url__c,
account.Environment_Code__c,
account.CLM_Account_Id__c,
account.CLM_Download_Named_Credential__c
);
String fileName = String.isNotBlank(downloaded.fileName)
? downloaded.fileName
: 'Generated_' + appraiserCase.Name + '.docx';
String title = fileName.contains('.')
? fileName.substringBeforeLast('.')
: fileName;
ContentVersion version = new ContentVersion(
Title = title,
PathOnClient = '/' + fileName,
VersionData = downloaded.body
);
insert version;
version = [
SELECT Id, ContentDocumentId
FROM ContentVersion
WHERE Id = :version.Id
LIMIT 1
];
insert new ContentDocumentLink(
ContentDocumentId = version.ContentDocumentId,
LinkedEntityId = appraiserCase.Id,
ShareType = 'V',
Visibility = 'AllUsers'
);
String fileUrl = '/lightning/r/ContentDocument/' + version.ContentDocumentId + '/view';
update new Appraiser_Case__c(
Id = appraiserCase.Id,
Last_CLM_Account_Code__c = accountCode,
Attached_File_Content_Document_Id__c = version.ContentDocumentId,
Attached_File_Url__c = fileUrl
);
FileAttachmentResult result = new FileAttachmentResult();
result.success = true;
result.message = 'Generated document attached to the case.';
result.contentDocumentId = version.ContentDocumentId;
result.fileUrl = fileUrl;
result.fileTitle = title;
return result;
}
private static String formatAddress(Appraiser_Case__c appraiserCase) {
return AppraiserCasePayloadBuilder.formatMailingAddress(
appraiserCase.Property_Street__c,
appraiserCase.Property_City__c,
appraiserCase.Property_State_Province__c,
appraiserCase.Property_Postal_Code__c,
appraiserCase.Property_Country__c
);
}
@AuraEnabled(cacheable=false)
public static FolderContents getFolderContents(String folderHref, String accountCode) {
if (String.isBlank(folderHref)) {
throw new IllegalArgumentException('folderHref is required');
}
CLM_Account_Setting__mdt account = requireAccountSetting(accountCode);
FolderContents contents = new FolderContents();
contents.folder = parseSingleResource(performGet(folderHref, account).getBody(), 'Folder');
contents.folders = parseResourceList(performGet(folderHref + '/folders', account).getBody(), 'Folder', folderHref);
contents.documents = parseResourceList(performGet(folderHref + '/documents', account).getBody(), 'Document', folderHref);
return contents;
}
private static HttpResponse performGet(String resourceOrHref, CLM_Account_Setting__mdt account) {
HttpRequest req = new HttpRequest();
req.setEndpoint(CLMDocGenCallout.buildEndpointForResource(
resourceOrHref,
account.CLM_Account_Id__c,
account.CLM_Api_Named_Credential__c
));
req.setMethod('GET');
req.setTimeout(CLMDocGenCallout.HTTP_TIMEOUT);
HttpResponse res = new Http().send(req);
Integer statusCode = res.getStatusCode();
if (statusCode < 200 || statusCode >= 300) {
throw new AuraHandledException('CLM API Error (HTTP ' + statusCode + '): ' + res.getBody());
}
return res;
}
@TestVisible
private static ResourceItem parseSingleResource(String body, String defaultType) {
Object root = JSON.deserializeUntyped(body);
if (!(root instanceof Map<String, Object>)) {
return null;
}
return parseResource((Map<String, Object>) root, defaultType, null);
}
@TestVisible
private static List<ResourceItem> parseResourceList(String body, String defaultType, String parentHref) {
Object root = JSON.deserializeUntyped(body);
List<Object> records = unwrapList(root);
List<ResourceItem> items = new List<ResourceItem>();
for (Object record : records) {
if (record instanceof Map<String, Object>) {
items.add(parseResource((Map<String, Object>) record, defaultType, parentHref));
}
}
return items;
}
private static List<Object> unwrapList(Object root) {
if (root instanceof List<Object>) {
return (List<Object>) root;
}
if (root instanceof Map<String, Object>) {
Map<String, Object> payload = (Map<String, Object>) root;
for (String key : new List<String>{ 'Results', 'Items', 'Documents', 'Folders' }) {
Object value = payload.get(key);
if (value instanceof List<Object>) {
return (List<Object>) value;
}
}
if (payload.size() == 1) {
for (Object value : payload.values()) {
if (value instanceof List<Object>) {
return (List<Object>) value;
}
}
}
}
return new List<Object>();
}
private static ResourceItem parseResource(Map<String, Object> source, String defaultType, String parentHref) {
ResourceItem item = new ResourceItem();
item.name = firstString(source, new List<String>{ 'Name', 'DisplayName', 'Title', 'Label' });
item.href = firstString(source, new List<String>{ 'Href', 'Uri', 'Location' });
item.type = firstString(source, new List<String>{ 'Type', 'ObjectType', 'ItemType' });
item.parentHref = extractParentHref(source, parentHref);
item.rawJson = JSON.serialize(source);
if (String.isBlank(item.type)) {
item.type = defaultType;
}
if (String.isBlank(item.name) && String.isNotBlank(item.href)) {
item.name = item.href.substringAfterLast('/');
}
return item;
}
private static String extractParentHref(Map<String, Object> source, String fallbackValue) {
Object parentValue = source.get('Parent');
if (parentValue instanceof Map<String, Object>) {
String href = firstString((Map<String, Object>) parentValue, new List<String>{ 'Href', 'Uri', 'Location' });
if (String.isNotBlank(href)) {
return href;
}
}
return fallbackValue;
}
private static String firstString(Map<String, Object> source, List<String> keys) {
for (String key : keys) {
Object value = source.get(key);
if (value != null) {
String textValue = String.valueOf(value);
if (String.isNotBlank(textValue)) {
return textValue;
}
}
}
return null;
}
private static void persistDocGenResult(
Id appraiserCaseId,
String templateDocHref,
String destinationFolderHref,
CLMDocGenCallout.CLMDocGenResponse response,
Boolean isStatusRefresh,
String accountCode
) {
if (appraiserCaseId == null || response == null) {
return;
}
Appraiser_Case__c updateCase = new Appraiser_Case__c(Id = appraiserCaseId);
updateCase.Last_CLM_Account_Code__c = accountCode;
updateCase.Last_DocGen_Status__c = String.isNotBlank(response.taskStatus) ? response.taskStatus : (response.success ? 'Submitted' : 'Failed');
updateCase.Last_DocGen_Message__c = response.message;
updateCase.Last_DocGen_Task_Id__c = response.documentId;
updateCase.Last_DocGen_Task_Url__c = response.documentUrl;
if (!isStatusRefresh) {
updateCase.Last_DocGen_Requested_At__c = System.now();
updateCase.Last_Template_Document_Href__c = templateDocHref;
updateCase.Last_Destination_Folder_Href__c = destinationFolderHref;
}
if (String.isNotBlank(response.generatedDocumentUrl)) {
updateCase.Generated_Document_Url__c = response.generatedDocumentUrl;
updateCase.Generated_Document_Id__c = response.generatedDocumentId;
}
if (response.success && String.valueOf(response.taskStatus).toLowerCase() == 'completed') {
updateCase.Last_DocGen_Completed_At__c = System.now();
}
update updateCase;
}
private static CLM_Account_Setting__mdt requireAccountSetting(String accountCode) {
CLM_Account_Setting__mdt row = resolveAccountSetting(accountCode);
if (row == null) {
throw new AuraHandledException('No active CLM account setting was found for ' + accountCode + '.');
}
return row;
}
private static CLM_Account_Setting__mdt resolveAccountSetting(String accountCode) {
String normalizedCode = String.isBlank(accountCode) ? null : accountCode.trim();
List<CLM_Account_Setting__mdt> rows;
if (String.isNotBlank(normalizedCode)) {
rows = [
SELECT DeveloperName,
Account_Code__c,
Account_Display_Name__c,
Environment_Code__c,
CLM_Account_Id__c,
CLM_Api_Named_Credential__c,
CLM_Download_Named_Credential__c,
ESignature_Rest_Named_Credential__c,
Template_Root_Folder_Href__c,
Destination_Root_Folder_Href__c,
Default_Template_Document_Href__c,
Default_Destination_Document_Name_Prefix__c,
Active__c
FROM CLM_Account_Setting__mdt
WHERE Active__c = true
AND DeveloperName = :normalizedCode
LIMIT 1
];
if (rows.isEmpty()) {
rows = [
SELECT DeveloperName,
Account_Code__c,
Account_Display_Name__c,
Environment_Code__c,
CLM_Account_Id__c,
CLM_Api_Named_Credential__c,
CLM_Download_Named_Credential__c,
ESignature_Rest_Named_Credential__c,
Template_Root_Folder_Href__c,
Destination_Root_Folder_Href__c,
Default_Template_Document_Href__c,
Default_Destination_Document_Name_Prefix__c,
Active__c
FROM CLM_Account_Setting__mdt
WHERE Active__c = true
AND Account_Code__c = :normalizedCode
LIMIT 1
];
}
} else {
rows = [
SELECT DeveloperName,
Account_Code__c,
Account_Display_Name__c,
Environment_Code__c,
CLM_Account_Id__c,
CLM_Api_Named_Credential__c,
CLM_Download_Named_Credential__c,
ESignature_Rest_Named_Credential__c,
Template_Root_Folder_Href__c,
Destination_Root_Folder_Href__c,
Default_Template_Document_Href__c,
Default_Destination_Document_Name_Prefix__c,
Active__c
FROM CLM_Account_Setting__mdt
WHERE Active__c = true
ORDER BY Account_Display_Name__c ASC, DeveloperName ASC
LIMIT 1
];
}
return rows.isEmpty() ? null : rows[0];
}
private static AccountSettings toAccountSettings(CLM_Account_Setting__mdt row) {
AccountSettings settings = new AccountSettings();
settings.accountCode = String.isNotBlank(row.Account_Code__c) ? row.Account_Code__c : row.DeveloperName;
settings.accountDisplayName = String.isNotBlank(row.Account_Display_Name__c) ? row.Account_Display_Name__c : row.DeveloperName;
settings.environment = row.Environment_Code__c;
settings.clmAccountId = row.CLM_Account_Id__c;
settings.clmApiNamedCredential = row.CLM_Api_Named_Credential__c;
settings.clmDownloadNamedCredential = row.CLM_Download_Named_Credential__c;
settings.eSignatureRestNamedCredential = row.ESignature_Rest_Named_Credential__c;
settings.templateRootFolderHref = row.Template_Root_Folder_Href__c;
settings.destinationRootFolderHref = row.Destination_Root_Folder_Href__c;
settings.defaultTemplateDocumentHref = row.Default_Template_Document_Href__c;
settings.defaultDocumentNamePrefix = row.Default_Destination_Document_Name_Prefix__c;
settings.active = row.Active__c;
return settings;
}
private static LetterSettings toLetterSettings(CLM_Letter_Definition__mdt row, AccountSettings account) {
LetterSettings settings = new LetterSettings();
settings.accountCode = String.isNotBlank(row.Account_Code__c) ? row.Account_Code__c : account.accountCode;
settings.letterCode = String.isNotBlank(row.Letter_Code__c) ? row.Letter_Code__c : 'APPRAISER_REVIEW';
settings.letterDisplayName = String.isNotBlank(row.Letter_Display_Name__c) ? row.Letter_Display_Name__c : row.DeveloperName;
settings.description = row.Description__c;
settings.isDefault = row.Is_Default__c;
settings.sortOrder = row.Sort_Order__c;
settings.templateRootFolderHref = firstNonBlankValue(row.Template_Root_Folder_Href__c, account.templateRootFolderHref);
settings.destinationRootFolderHref = firstNonBlankValue(row.Destination_Root_Folder_Href__c, account.destinationRootFolderHref);
settings.defaultTemplateDocumentHref = firstNonBlankValue(row.Default_Template_Document_Href__c, account.defaultTemplateDocumentHref);
settings.defaultDocumentNamePrefix = firstNonBlankValue(row.Default_Destination_Document_Name_Prefix__c, account.defaultDocumentNamePrefix);
settings.active = row.Active__c;
return settings;
}
private static LetterSettings buildFallbackLetterSettings(AccountSettings account) {
LetterSettings settings = new LetterSettings();
settings.accountCode = account.accountCode;
settings.letterCode = 'APPRAISER_REVIEW';
settings.letterDisplayName = 'Appraiser Review Letter';
settings.description = 'Fallback current appraiser letter flow.';
settings.isDefault = true;
settings.sortOrder = 10;
settings.templateRootFolderHref = account.templateRootFolderHref;
settings.destinationRootFolderHref = account.destinationRootFolderHref;
settings.defaultTemplateDocumentHref = account.defaultTemplateDocumentHref;
settings.defaultDocumentNamePrefix = account.defaultDocumentNamePrefix;
settings.active = true;
return settings;
}
private static String firstNonBlankValue(String preferredValue, String fallbackValue) {
return String.isNotBlank(preferredValue) ? preferredValue : fallbackValue;
}
private static String buildDefaultDocumentName(String prefix, String caseNumber) {
String normalizedPrefix = String.isNotBlank(prefix) ? prefix : 'Review';
return String.isNotBlank(caseNumber)
? normalizedPrefix + '_' + caseNumber + '.docx'
: normalizedPrefix + '.docx';
}
}

View File

@ -0,0 +1,286 @@
@IsTest
private class CLMAdminServiceTest {
private class FolderBrowseMock implements HttpCalloutMock {
public HttpResponse respond(HttpRequest req) {
HttpResponse res = new HttpResponse();
res.setHeader('Content-Type', 'application/json');
if (req.getMethod() == 'POST' && req.getEndpoint().contains('/documentxmlmergetasks')) {
res.setStatusCode(200);
res.setBody('{"Href":"https://apiuatna11.springcm.com/v2/bccae332-c7db-4892-ab85-257df0f70fea/documentxmlmergetasks/TASK-555","Status":"Queued","Result":{"Href":"https://apiuatna11.springcm.com/v2/bccae332-c7db-4892-ab85-257df0f70fea/documents/generated-555"}}');
return res;
}
if (req.getEndpoint().endsWith('/folders/root-folder')) {
res.setStatusCode(200);
res.setBody('{"Name":"Templates Root","Href":"https://uatna11.springcm.com/v2/2371cf36-eb8a-43fe-9f28-b5bbe7644397/folders/root-folder","Type":"Folder","Parent":{"Href":"https://uatna11.springcm.com/v2/2371cf36-eb8a-43fe-9f28-b5bbe7644397/folders/root-parent"}}');
return res;
}
if (req.getEndpoint().endsWith('/folders/root-folder/folders')) {
res.setStatusCode(200);
res.setBody('{"Results":[{"Name":"Residential","Href":"https://uatna11.springcm.com/v2/2371cf36-eb8a-43fe-9f28-b5bbe7644397/folders/residential","Parent":{"Href":"https://uatna11.springcm.com/v2/2371cf36-eb8a-43fe-9f28-b5bbe7644397/folders/root-folder"}}]}');
return res;
}
if (req.getEndpoint().endsWith('/folders/root-folder/documents')) {
res.setStatusCode(200);
res.setBody('{"Results":[{"Name":"Appraiser Review Letter Template","Href":"https://uatna11.springcm.com/v2/2371cf36-eb8a-43fe-9f28-b5bbe7644397/documents/template-1"}]}');
return res;
}
if (req.getMethod() == 'GET' && req.getEndpoint().contains('/documentxmlmergetasks')) {
res.setStatusCode(200);
res.setBody('{"Href":"https://apiuatna11.springcm.com/v2/bccae332-c7db-4892-ab85-257df0f70fea/documentxmlmergetasks/TASK-555","Status":"Completed","Result":{"Href":"https://apiuatna11.springcm.com/v2/bccae332-c7db-4892-ab85-257df0f70fea/documents/generated-555"}}');
return res;
}
if (req.getMethod() == 'GET' && req.getEndpoint().contains('/documents/generated-555')) {
res.setStatusCode(200);
res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document');
res.setHeader('Content-Disposition', 'attachment; filename="Review_AC-000001.docx"');
res.setBodyAsBlob(Blob.valueOf('docx-binary'));
return res;
}
res.setStatusCode(404);
res.setBody('{"Message":"Not Found"}');
return res;
}
}
@IsTest
static void parsesFolderContents() {
Test.setMock(HttpCalloutMock.class, new FolderBrowseMock());
Test.startTest();
CLMAdminService.FolderContents contents = CLMAdminService.getFolderContents(
'https://uatna11.springcm.com/v2/2371cf36-eb8a-43fe-9f28-b5bbe7644397/folders/root-folder',
'DTC_CLM_Demo'
);
String body = CLMAdminService.probeResource('/documentxmlmergetasks/TASK-555', 'DTC_IAM_Enterprise');
Test.stopTest();
System.assertEquals('Templates Root', contents.folder.name);
System.assertEquals('https://uatna11.springcm.com/v2/2371cf36-eb8a-43fe-9f28-b5bbe7644397/folders/root-parent', contents.folder.parentHref);
System.assertEquals(1, contents.folders.size());
System.assertEquals('Residential', contents.folders[0].name);
System.assertEquals(1, contents.documents.size());
System.assertEquals('Appraiser Review Letter Template', contents.documents[0].name);
System.assert(body.contains('Completed'));
}
@IsTest
static void returnsAccountSettingsFromMetadata() {
Test.startTest();
List<CLMAdminService.AccountSettings> accounts = CLMAdminService.listAccountSettings();
CLMAdminService.AccountSettings settings = CLMAdminService.getAccountSettings('DTC_CLM_Demo');
Test.stopTest();
Set<String> accountCodes = new Set<String>();
for (CLMAdminService.AccountSettings account : accounts) {
accountCodes.add(account.accountCode);
}
System.assert(accountCodes.contains('DTC_CLM_Demo'));
System.assert(accountCodes.contains('DTC_IAM_Enterprise'));
System.assert(accountCodes.contains('DTC_HUD_Demo'));
System.assertNotEquals(null, settings);
System.assertEquals('DTC_CLM_Demo', settings.accountCode);
System.assertEquals('DTC CLM Demo', settings.accountDisplayName);
System.assertEquals('UAT', settings.environment);
System.assert(settings.destinationRootFolderHref.contains('/folders/'));
System.assert(settings.defaultTemplateDocumentHref.contains('/documents/'));
System.assertEquals('Review', settings.defaultDocumentNamePrefix);
}
@IsTest
static void returnsLetterSettingsFromMetadata() {
Test.startTest();
List<CLMAdminService.LetterSettings> letters = CLMAdminService.listLetterSettings('DTC_CLM_Demo');
CLMAdminService.LetterSettings settings = CLMAdminService.getLetterSettings('DTC_CLM_Demo', 'APPRAISER_REVIEW');
Test.stopTest();
System.assert(!letters.isEmpty());
System.assertEquals('APPRAISER_REVIEW', settings.letterCode);
System.assertEquals('Appraiser Review Letter', settings.letterDisplayName);
System.assertEquals(true, settings.isDefault);
System.assertEquals('Review', settings.defaultDocumentNamePrefix);
}
@IsTest
static void buildsDocGenPreviewForSelectedLetter() {
Appraiser_Case__c appraiserCase = new Appraiser_Case__c(
Appraiser_Field_Review_Date__c = Date.today(),
Letter_Sent_Date__c = Date.newInstance(2026, 4, 9),
FHA_Case_Number__c = '123-4567890',
Appraiser_Name__c = 'Jamie Carter',
Appraiser_Last_Name__c = 'Carter',
Appraiser_Street__c = '12 Park Ave',
Appraiser_City__c = 'New York',
Appraiser_State_Province__c = 'NY',
Appraiser_Postal_Code__c = '10016',
Appraiser_Country__c = 'USA',
Property_Street__c = '245 Lexington Ave',
Property_City__c = 'New York',
Property_State_Province__c = 'NY',
Property_Postal_Code__c = '10016',
Property_Country__c = 'USA'
);
insert appraiserCase;
insert new Appraiser_Case_Deficiency__c(
Appraiser_Case__c = appraiserCase.Id,
Deficiency_Number__c = 1,
Description__c = 'Test deficiency',
Resolution__c = 'Test resolution',
Reference__c = 'VC-1'
);
Test.startTest();
CLMAdminService.DocGenPreview preview = CLMAdminService.getDocGenPreview(
appraiserCase.Id,
'DTC_CLM_Demo',
'NOD_LETTER'
);
Test.stopTest();
System.assertEquals('DTC_CLM_Demo', preview.accountCode);
System.assertEquals('NOD_LETTER', preview.letterCode);
System.assertEquals('NOD Letter', preview.letterDisplayName);
Appraiser_Case__c refreshedCase = [
SELECT Name
FROM Appraiser_Case__c
WHERE Id = :appraiserCase.Id
LIMIT 1
];
System.assertEquals('NOD_' + refreshedCase.Name + '.docx', preview.destinationDocName);
System.assert(preview.mergeTaskEndpointUrl.endsWith('/documentxmlmergetasks'));
System.assert(preview.payloadJson.contains('FHACaseNumber'));
System.assert(preview.dataXml.contains('\n <'));
System.assert(preview.dataXml.contains('<Reference>VC-1</Reference>'));
System.assert(preview.requestBodyJson.contains('"DestinationDocumentName"'));
}
@IsTest
static void persistsCaseTrackingOnGenerate() {
Appraiser_Case__c appraiserCase = new Appraiser_Case__c(
Appraiser_Field_Review_Date__c = Date.today(),
Property_Street__c = '123 Main St',
Property_City__c = 'Denver',
Property_State_Province__c = 'CO',
Property_Postal_Code__c = '80202'
);
insert appraiserCase;
insert new Appraiser_Case_Deficiency__c(
Appraiser_Case__c = appraiserCase.Id,
Deficiency_Number__c = 1,
Description__c = 'Test deficiency',
Resolution__c = 'Test resolution',
Reference__c = 'VC-1'
);
Test.setMock(HttpCalloutMock.class, new FolderBrowseMock());
Test.startTest();
CLMDocGenCallout.CLMDocGenResponse generateResponse = CLMAdminService.generateDocument(
appraiserCase.Id,
'https://apiuatna11.springcm.com/v2/bccae332-c7db-4892-ab85-257df0f70fea/documents/template-1',
'https://apiuatna11.springcm.com/v2/bccae332-c7db-4892-ab85-257df0f70fea/folders/root-folder',
'Review_AC-000001.docx',
'DTC_CLM_Demo'
);
CLMAdminService.CaseContext context = CLMAdminService.getCaseContext(appraiserCase.Id);
Test.stopTest();
System.assertEquals(true, generateResponse.success);
System.assertEquals('Queued', context.lastDocGenStatus);
System.assertEquals('DTC_CLM_Demo', context.lastClmAccountCode);
System.assertEquals('TASK-555', context.lastDocGenTaskId);
System.assert(context.lastDocGenTaskUrl.contains('/documentxmlmergetasks/TASK-555'));
System.assert(context.generatedDocumentUrl.contains('/documents/generated-555'));
System.assertNotEquals(null, context.lastDocGenRequestedAt);
System.assertEquals(null, context.lastDocGenCompletedAt);
System.assertEquals(1, context.deficiencies.size());
System.assertEquals('123 Main St, Denver, CO 80202', context.propertyAddress);
System.assertEquals('VC-1', context.deficiencies[0].reference);
}
@IsTest
static void persistsCaseTrackingOnStatusRefresh() {
Appraiser_Case__c appraiserCase = new Appraiser_Case__c(
Appraiser_Field_Review_Date__c = Date.today(),
Property_Street__c = '123 Main St',
Property_City__c = 'Denver',
Property_State_Province__c = 'CO',
Property_Postal_Code__c = '80202',
Last_DocGen_Task_Id__c = 'TASK-555'
);
insert appraiserCase;
insert new Appraiser_Case_Deficiency__c(
Appraiser_Case__c = appraiserCase.Id,
Deficiency_Number__c = 1,
Description__c = 'Test deficiency',
Resolution__c = 'Test resolution',
Reference__c = 'VC-1'
);
Test.setMock(HttpCalloutMock.class, new FolderBrowseMock());
Test.startTest();
CLMDocGenCallout.CLMDocGenResponse statusResponse = CLMAdminService.getTaskStatus(appraiserCase.Id, 'TASK-555', 'DTC_CLM_Demo');
CLMAdminService.CaseContext context = CLMAdminService.getCaseContext(appraiserCase.Id);
Test.stopTest();
System.assertEquals(true, statusResponse.success);
System.assertEquals('Completed', context.lastDocGenStatus);
System.assertEquals('DTC_CLM_Demo', context.lastClmAccountCode);
System.assertEquals('TASK-555', context.lastDocGenTaskId);
System.assert(context.lastDocGenTaskUrl.contains('/documentxmlmergetasks/TASK-555'));
System.assert(context.generatedDocumentUrl.contains('/documents/generated-555'));
System.assertNotEquals(null, context.lastDocGenCompletedAt);
System.assertEquals(1, context.deficiencies.size());
}
@IsTest
static void attachesGeneratedDocumentToCaseAsSalesforceFile() {
Appraiser_Case__c appraiserCase = new Appraiser_Case__c(
Appraiser_Field_Review_Date__c = Date.today(),
Property_Street__c = '245 Lexington Ave',
Property_City__c = 'New York',
Property_State_Province__c = 'NY',
Property_Postal_Code__c = '10016',
Generated_Document_Url__c = 'https://apiuatna11.springcm.com/v2/bccae332-c7db-4892-ab85-257df0f70fea/documents/generated-555',
Generated_Document_Id__c = 'generated-555'
);
insert appraiserCase;
Test.setMock(HttpCalloutMock.class, new FolderBrowseMock());
Test.startTest();
CLMAdminService.FileAttachmentResult result = CLMAdminService.attachGeneratedDocumentToCase(appraiserCase.Id, 'DTC_CLM_Demo');
Test.stopTest();
Appraiser_Case__c refreshedCase = [
SELECT Last_CLM_Account_Code__c, Attached_File_Content_Document_Id__c, Attached_File_Url__c
FROM Appraiser_Case__c
WHERE Id = :appraiserCase.Id
LIMIT 1
];
List<ContentDocumentLink> links = [
SELECT ContentDocumentId, LinkedEntityId
FROM ContentDocumentLink
WHERE LinkedEntityId = :appraiserCase.Id
];
System.assertEquals(true, result.success);
System.assertNotEquals(null, result.contentDocumentId);
System.assert(result.fileUrl.contains('/lightning/r/ContentDocument/'));
System.assertEquals('DTC_CLM_Demo', refreshedCase.Last_CLM_Account_Code__c);
System.assertEquals(result.contentDocumentId, refreshedCase.Attached_File_Content_Document_Id__c);
System.assertEquals(result.fileUrl, refreshedCase.Attached_File_Url__c);
System.assertEquals(1, links.size());
System.assertEquals(result.contentDocumentId, links[0].ContentDocumentId);
}
}

View File

@ -2,17 +2,94 @@ public class CLMDocGenCallout {
// S1 demo environment // S1 demo environment
private static final String CLM_ACCOUNT_ID_S1 = '2371cf36-eb8a-43fe-9f28-b5bbe7644397'; private static final String CLM_ACCOUNT_ID_S1 = '2371cf36-eb8a-43fe-9f28-b5bbe7644397';
private static final String CLM_BASE_S1 = 'callout:CLMNamedCred/v2/' + CLM_ACCOUNT_ID_S1; private static final String CLM_NAMED_CRED_S1 = 'callout:CLMs1NamedCreds';
private static final String CLM_DOWNLOAD_NAMED_CRED_S1 = 'callout:CLMs1Download';
// UAT demo environment // UAT demo environment
private static final String CLM_ACCOUNT_ID_UAT = 'bccae332-c7db-4892-ab85-257df0f70fea'; private static final String CLM_ACCOUNT_ID_UAT = 'bccae332-c7db-4892-ab85-257df0f70fea';
private static final String CLM_BASE_UAT = 'callout:CLMuatNamedCreds/v2/' + CLM_ACCOUNT_ID_UAT; private static final String CLM_NAMED_CRED_UAT = 'callout:CLMuatNamedCreds';
private static final String CLM_DOWNLOAD_NAMED_CRED_UAT = 'callout:CLMuatDownload';
private static final Integer HTTP_TIMEOUT = 30000; public static final Integer HTTP_TIMEOUT = 30000;
/** Resolve the correct CLM base URL. env: 'UAT' or 'S1'. */ /** Resolve the correct CLM Named Credential base. env: 'UAT' or 'S1'. */
private static String clmBase(String env) { private static String clmNamedCredential(String env) {
return env == 'S1' ? CLM_BASE_S1 : CLM_BASE_UAT; return env == 'S1' ? CLM_NAMED_CRED_S1 : CLM_NAMED_CRED_UAT;
}
private static String clmDownloadNamedCredential(String env) {
return env == 'S1' ? CLM_DOWNLOAD_NAMED_CRED_S1 : CLM_DOWNLOAD_NAMED_CRED_UAT;
}
@TestVisible
static String defaultAccountId(String env) {
return env == 'S1' ? CLM_ACCOUNT_ID_S1 : CLM_ACCOUNT_ID_UAT;
}
@TestVisible
static String extractAccountId(String resourceOrHref) {
if (String.isBlank(resourceOrHref)) {
return null;
}
Integer v2Index = resourceOrHref.indexOf('/v2/');
if (v2Index < 0) {
return null;
}
String afterV2 = resourceOrHref.substring(v2Index + 4);
List<String> parts = afterV2.split('/');
return parts.isEmpty() ? null : parts[0];
}
@TestVisible
static String normalizeResourcePath(String resourceOrHref, String env) {
return normalizeResourcePathWithAccountId(resourceOrHref, defaultAccountId(env));
}
@TestVisible
static String normalizeResourcePathWithAccountId(String resourceOrHref, String fallbackAccountId) {
if (String.isBlank(resourceOrHref)) {
throw new IllegalArgumentException('resourceOrHref is required');
}
if (resourceOrHref.startsWith('http://') || resourceOrHref.startsWith('https://')) {
Integer pathStart = resourceOrHref.indexOf('/', resourceOrHref.indexOf('//') + 2);
if (pathStart < 0) {
throw new IllegalArgumentException('Unable to determine resource path from href: ' + resourceOrHref);
}
return resourceOrHref.substring(pathStart);
}
if (resourceOrHref.startsWith('/v2/')) {
return resourceOrHref;
}
if (resourceOrHref.startsWith('v2/')) {
return '/' + resourceOrHref;
}
if (resourceOrHref.startsWith('/')) {
return '/v2/' + fallbackAccountId + resourceOrHref;
}
return '/v2/' + fallbackAccountId + '/' + resourceOrHref;
}
public static String buildEndpointForResource(String resourceOrHref, String env) {
return clmNamedCredential(env) + normalizeResourcePath(resourceOrHref, env);
}
public static String buildEndpointForResource(String resourceOrHref, String accountId, String apiNamedCredential) {
return 'callout:' + apiNamedCredential + normalizeResourcePathWithAccountId(resourceOrHref, accountId);
}
public static String buildDownloadEndpointForResource(String resourceOrHref, String env) {
return clmDownloadNamedCredential(env) + normalizeResourcePath(resourceOrHref, env);
}
public static String buildDownloadEndpointForResource(String resourceOrHref, String accountId, String downloadNamedCredential) {
return 'callout:' + downloadNamedCredential + normalizeResourcePathWithAccountId(resourceOrHref, accountId);
} }
/** Defaults to UAT environment. */ /** Defaults to UAT environment. */
@ -40,21 +117,42 @@ public class CLMDocGenCallout {
String destinationDocName, String destinationDocName,
String env String env
) { ) {
Map<String, Object> casePayload = AppraiserCasePayloadBuilder.buildPayload(caseId); return generateDocument(
caseId,
Map<String, Object> requestBody = new Map<String, Object>{ templateDocHref,
'TemplateDocument' => new Map<String, Object>{ destinationFolderHref,
'Href' => templateDocHref destinationDocName,
}, env,
'DataXML' => buildDataXml(casePayload), defaultAccountId(env),
'DestinationDocumentName' => destinationDocName, clmNamedCredential(env).substringAfter('callout:')
'DestinationFolder' => new Map<String, Object>{ );
'Href' => destinationFolderHref
} }
};
public static CLMDocGenResponse generateDocument(
String caseId,
String templateDocHref,
String destinationFolderHref,
String destinationDocName,
String env,
String configuredAccountId,
String apiNamedCredential
) {
Map<String, Object> casePayload = AppraiserCasePayloadBuilder.buildPayload(caseId);
String accountId = firstNonBlank(
extractAccountId(templateDocHref),
extractAccountId(destinationFolderHref),
configuredAccountId,
defaultAccountId(env)
);
Map<String, Object> requestBody = buildRequestBodyMap(
casePayload,
templateDocHref,
destinationFolderHref,
destinationDocName
);
HttpRequest req = new HttpRequest(); HttpRequest req = new HttpRequest();
req.setEndpoint(clmBase(env) + '/documentxmlmergetasks'); req.setEndpoint('callout:' + apiNamedCredential + '/v2/' + accountId + '/documentxmlmergetasks');
req.setMethod('POST'); req.setMethod('POST');
req.setHeader('Content-Type', 'application/json'); req.setHeader('Content-Type', 'application/json');
req.setTimeout(HTTP_TIMEOUT); req.setTimeout(HTTP_TIMEOUT);
@ -69,15 +167,59 @@ public class CLMDocGenCallout {
} }
} }
/** Poll the status of a submitted merge task by its GUID. */ public static String buildDataXmlForCase(String caseId) {
return buildDataXml(AppraiserCasePayloadBuilder.buildPayload(caseId));
}
public static String buildRequestBodyJson(
String caseId,
String templateDocHref,
String destinationFolderHref,
String destinationDocName
) {
return JSON.serializePretty(
buildRequestBodyMap(
AppraiserCasePayloadBuilder.buildPayload(caseId),
templateDocHref,
destinationFolderHref,
destinationDocName
)
);
}
public static String buildDocumentXmlMergeTasksUrl(
String templateDocHref,
String destinationFolderHref,
String env,
String configuredAccountId
) {
String accountId = firstNonBlank(
extractAccountId(templateDocHref),
extractAccountId(destinationFolderHref),
configuredAccountId,
defaultAccountId(env)
);
String baseUrl = firstNonBlank(
extractBaseUrl(templateDocHref),
extractBaseUrl(destinationFolderHref),
null,
defaultBaseUrl(env)
);
return baseUrl + '/v2/' + accountId + '/documentxmlmergetasks';
}
/** Poll the status of a submitted merge task by its GUID (defaults to UAT). */ /** Poll the status of a submitted merge task by its GUID (defaults to UAT). */
public static CLMDocGenResponse getTaskStatus(String taskId) { public static CLMDocGenResponse getTaskStatus(String taskId) {
return getTaskStatus(taskId, 'UAT'); return getTaskStatus(taskId, 'UAT');
} }
public static CLMDocGenResponse getTaskStatus(String taskId, String env) { public static CLMDocGenResponse getTaskStatus(String taskId, String env) {
return getTaskStatus(taskId, env, defaultAccountId(env), clmNamedCredential(env).substringAfter('callout:'));
}
public static CLMDocGenResponse getTaskStatus(String taskId, String env, String configuredAccountId, String apiNamedCredential) {
HttpRequest req = new HttpRequest(); HttpRequest req = new HttpRequest();
req.setEndpoint(clmBase(env) + '/documentxmlmergetasks/' + taskId); req.setEndpoint(buildEndpointForResource('/documentxmlmergetasks/' + taskId, configuredAccountId, apiNamedCredential));
req.setMethod('GET'); req.setMethod('GET');
req.setTimeout(HTTP_TIMEOUT); req.setTimeout(HTTP_TIMEOUT);
try { try {
@ -96,13 +238,41 @@ public class CLMDocGenCallout {
public static String probe(String resource, String env) { public static String probe(String resource, String env) {
HttpRequest req = new HttpRequest(); HttpRequest req = new HttpRequest();
req.setEndpoint(clmBase(env) + '/' + resource); req.setEndpoint(buildEndpointForResource(resource, env));
req.setMethod('GET'); req.setMethod('GET');
req.setTimeout(HTTP_TIMEOUT); req.setTimeout(HTTP_TIMEOUT);
HttpResponse res = new Http().send(req); HttpResponse res = new Http().send(req);
return 'HTTP ' + res.getStatusCode() + ': ' + res.getBody(); return 'HTTP ' + res.getStatusCode() + ': ' + res.getBody();
} }
public static DownloadedDocument downloadDocument(String resourceOrHref, String env) {
return downloadDocument(
resourceOrHref,
env,
defaultAccountId(env),
clmDownloadNamedCredential(env).substringAfter('callout:')
);
}
public static DownloadedDocument downloadDocument(String resourceOrHref, String env, String configuredAccountId, String downloadNamedCredential) {
HttpRequest req = new HttpRequest();
req.setEndpoint(buildDownloadEndpointForResource(resourceOrHref, configuredAccountId, downloadNamedCredential));
req.setMethod('GET');
req.setTimeout(HTTP_TIMEOUT);
HttpResponse res = new Http().send(req);
Integer statusCode = res.getStatusCode();
if (statusCode < 200 || statusCode >= 300) {
throw new CalloutException('CLM download error (HTTP ' + statusCode + '): ' + res.getBody());
}
DownloadedDocument document = new DownloadedDocument();
document.body = res.getBodyAsBlob();
document.contentType = res.getHeader('Content-Type');
document.fileName = extractFileName(res, resourceOrHref, document.contentType);
return document;
}
/** /**
* Build the DataXML string from the case payload. * Build the DataXML string from the case payload.
* Flat fields become direct child elements of <TemplateFieldData>. * Flat fields become direct child elements of <TemplateFieldData>.
@ -115,7 +285,7 @@ public class CLMDocGenCallout {
// Emit flat fields first // Emit flat fields first
for (String key : payload.keySet()) { for (String key : payload.keySet()) {
if (key == 'DeficiencyList') continue; if (key == 'DeficiencyList') continue;
xml += '<' + key + '>' + escapeXml(String.valueOf(payload.get(key))) + '</' + key + '>'; xml += '<' + key + '>' + escapeXml(safeValue(payload.get(key))) + '</' + key + '>';
} }
// Emit DeficiencyList as a nested list so templates can iterate dynamically // Emit DeficiencyList as a nested list so templates can iterate dynamically
@ -125,9 +295,10 @@ public class CLMDocGenCallout {
for (Integer i = 0; i < deficiencies.size(); i++) { for (Integer i = 0; i < deficiencies.size(); i++) {
Map<String, Object> d = (Map<String, Object>) deficiencies[i]; Map<String, Object> d = (Map<String, Object>) deficiencies[i];
xml += '<Deficiency>'; xml += '<Deficiency>';
xml += '<Number>' + escapeXml(String.valueOf(d.get('deficiencyNumber'))) + '</Number>'; xml += '<Number>' + escapeXml(safeValue(d.get('deficiencyNumber'))) + '</Number>';
xml += '<Description>' + escapeXml(String.valueOf(d.get('description'))) + '</Description>'; xml += '<Description>' + escapeXml(safeValue(d.get('description'))) + '</Description>';
xml += '<Resolution>' + escapeXml(String.valueOf(d.get('resolution'))) + '</Resolution>'; xml += '<Resolution>' + escapeXml(safeValue(d.get('resolution'))) + '</Resolution>';
xml += '<Reference>' + escapeXml(safeValue(d.get('reference'))) + '</Reference>';
xml += '</Deficiency>'; xml += '</Deficiency>';
} }
xml += '</DeficiencyList>'; xml += '</DeficiencyList>';
@ -138,6 +309,93 @@ public class CLMDocGenCallout {
return xml; return xml;
} }
public static String prettyPrintXml(String xml) {
if (String.isBlank(xml)) {
return xml;
}
String normalized = xml
.replace('><', '>\n<')
.replace('\r\n', '\n')
.replace('\r', '\n');
List<String> lines = normalized.split('\n');
List<String> formatted = new List<String>();
Integer indent = 0;
for (String rawLine : lines) {
String line = rawLine == null ? '' : rawLine.trim();
if (line == '') {
continue;
}
Boolean isClosing = line.startsWith('</');
Boolean isDeclaration = line.startsWith('<?') || line.startsWith('<!');
Boolean isSelfClosing = line.endsWith('/>') || (line.contains('</') && line.indexOf('</') > 0);
if (isClosing && indent > 0) {
indent--;
}
formatted.add(repeatIndent(indent) + line);
if (!isClosing && !isSelfClosing && !isDeclaration) {
indent++;
}
}
return String.join(formatted, '\n');
}
private static Map<String, Object> buildRequestBodyMap(
Map<String, Object> casePayload,
String templateDocHref,
String destinationFolderHref,
String destinationDocName
) {
return new Map<String, Object>{
'TemplateDocument' => new Map<String, Object>{
'Href' => templateDocHref
},
'DataXML' => buildDataXml(casePayload),
'DestinationDocumentName' => destinationDocName,
'DestinationFolder' => new Map<String, Object>{
'Href' => destinationFolderHref
}
};
}
private static String extractBaseUrl(String resourceOrHref) {
if (String.isBlank(resourceOrHref)) {
return null;
}
if (!(resourceOrHref.startsWith('http://') || resourceOrHref.startsWith('https://'))) {
return null;
}
Integer schemeIndex = resourceOrHref.indexOf('//');
Integer pathStart = resourceOrHref.indexOf('/', schemeIndex + 2);
return pathStart > 0 ? resourceOrHref.substring(0, pathStart) : resourceOrHref;
}
private static String defaultBaseUrl(String env) {
return env == 'S1'
? 'https://api.s1.us.clm.demo.docusign.net'
: 'https://apiuatna11.springcm.com';
}
private static String repeatIndent(Integer indent) {
String value = '';
for (Integer i = 0; i < indent; i++) {
value += ' ';
}
return value;
}
private static String safeValue(Object val) {
return val != null ? String.valueOf(val) : '';
}
private static String escapeXml(String s) { private static String escapeXml(String s) {
if (s == null) return ''; if (s == null) return '';
return s.replace('&', '&amp;') return s.replace('&', '&amp;')
@ -152,26 +410,160 @@ public class CLMDocGenCallout {
String body = res.getBody(); String body = res.getBody();
if (statusCode >= 200 && statusCode < 300) { if (statusCode >= 200 && statusCode < 300) {
Map<String, Object> m = (Map<String, Object>) JSON.deserializeUntyped(body); Map<String, Object> m = (Map<String, Object>) JSON.deserializeUntyped(body);
String href = (String) m.get('Href'); String href = firstString(m, new List<String>{ 'Href', 'Uri', 'Location' });
String status = (String) m.get('Status'); String status = firstString(m, new List<String>{ 'Status', 'State' });
String taskId = href != null ? href.substringAfterLast('/') : null; String taskId = href != null ? href.substringAfterLast('/') : null;
return new CLMDocGenResponse(true, 'Task status: ' + status, href, taskId); String generatedDocumentUrl = findFirstDocumentHref(m);
String generatedDocumentId = generatedDocumentUrl != null ? generatedDocumentUrl.substringAfterLast('/') : null;
String message = 'Task status: ' + (String.isNotBlank(status) ? status : 'Unknown');
return new CLMDocGenResponse(true, message, href, taskId, status, generatedDocumentUrl, generatedDocumentId, body);
} else { } else {
return new CLMDocGenResponse(false, 'CLM API Error (HTTP ' + statusCode + '): ' + body, null, null); return new CLMDocGenResponse(false, 'CLM API Error (HTTP ' + statusCode + '): ' + body, null, null, null, null, null, body);
} }
} }
private static String firstNonBlank(String firstValue, String secondValue, String thirdValue, String fallbackValue) {
if (String.isNotBlank(firstValue)) {
return firstValue;
}
if (String.isNotBlank(secondValue)) {
return secondValue;
}
if (String.isNotBlank(thirdValue)) {
return thirdValue;
}
return fallbackValue;
}
private static String firstString(Map<String, Object> source, List<String> keys) {
for (String key : keys) {
Object value = source.get(key);
if (value != null) {
String asString = String.valueOf(value);
if (String.isNotBlank(asString)) {
return asString;
}
}
}
return null;
}
private static String findFirstDocumentHref(Object node) {
if (node instanceof Map<String, Object>) {
Map<String, Object> mapNode = (Map<String, Object>) node;
String href = firstString(mapNode, new List<String>{ 'Href', 'Uri', 'Location' });
if (String.isNotBlank(href) && href.contains('/documents/') && !href.contains('/documentxmlmergetasks/')) {
return href;
}
for (Object value : mapNode.values()) {
String nestedHref = findFirstDocumentHref(value);
if (String.isNotBlank(nestedHref)) {
return nestedHref;
}
}
} else if (node instanceof List<Object>) {
for (Object item : (List<Object>) node) {
String nestedHref = findFirstDocumentHref(item);
if (String.isNotBlank(nestedHref)) {
return nestedHref;
}
}
}
return null;
}
private static String extractFileName(HttpResponse res, String resourceOrHref, String contentType) {
String disposition = res.getHeader('Content-Disposition');
if (String.isNotBlank(disposition)) {
Integer marker = disposition.toLowerCase().indexOf('filename=');
if (marker >= 0) {
String candidate = disposition.substring(marker + 9).trim();
if (candidate.startsWith('"') && candidate.endsWith('"') && candidate.length() >= 2) {
candidate = candidate.substring(1, candidate.length() - 1);
}
if (String.isNotBlank(candidate)) {
return candidate;
}
}
}
String baseName = String.isNotBlank(resourceOrHref) ? resourceOrHref.substringAfterLast('/') : 'generated-document';
String extension = inferExtension(contentType);
if (String.isNotBlank(extension) && !baseName.toLowerCase().endsWith(extension)) {
return baseName + extension;
}
return baseName;
}
private static String inferExtension(String contentType) {
if (String.isBlank(contentType)) {
return '.docx';
}
String normalizedType = contentType.toLowerCase();
if (normalizedType.contains('pdf')) {
return '.pdf';
}
if (normalizedType.contains('wordprocessingml') || normalizedType.contains('officedocument')) {
return '.docx';
}
if (normalizedType.contains('msword')) {
return '.doc';
}
if (normalizedType.contains('json')) {
return '.json';
}
return '';
}
public class CLMDocGenResponse { public class CLMDocGenResponse {
@AuraEnabled
public Boolean success; public Boolean success;
@AuraEnabled
public String message; public String message;
@AuraEnabled
public String documentUrl; public String documentUrl;
@AuraEnabled
public String documentId; public String documentId;
@AuraEnabled
public String taskStatus;
@AuraEnabled
public String generatedDocumentUrl;
@AuraEnabled
public String generatedDocumentId;
@AuraEnabled
public String taskDetailsJson;
public CLMDocGenResponse(Boolean success, String message, String documentUrl, String documentId) { public CLMDocGenResponse(Boolean success, String message, String documentUrl, String documentId) {
this(success, message, documentUrl, documentId, null, null, null, null);
}
public CLMDocGenResponse(
Boolean success,
String message,
String documentUrl,
String documentId,
String taskStatus,
String generatedDocumentUrl,
String generatedDocumentId,
String taskDetailsJson
) {
this.success = success; this.success = success;
this.message = message; this.message = message;
this.documentUrl = documentUrl; this.documentUrl = documentUrl;
this.documentId = documentId; this.documentId = documentId;
this.taskStatus = taskStatus;
this.generatedDocumentUrl = generatedDocumentUrl;
this.generatedDocumentId = generatedDocumentId;
this.taskDetailsJson = taskDetailsJson;
} }
} }
public class DownloadedDocument {
public Blob body;
public String fileName;
public String contentType;
}
} }

View File

@ -0,0 +1,155 @@
@IsTest
private class CLMDocGenCalloutTest {
private class CLMCalloutMock implements HttpCalloutMock {
public HttpResponse respond(HttpRequest req) {
HttpResponse res = new HttpResponse();
res.setHeader('Content-Type', 'application/json');
if (req.getMethod() == 'POST' && req.getEndpoint().contains('/documentxmlmergetasks')) {
System.assert(req.getBody().contains('TemplateDocument'));
System.assert(req.getBody().contains('DestinationFolder'));
System.assert(req.getBody().contains('DeficiencyList'));
System.assert(req.getBody().contains('&amp;'));
res.setStatusCode(200);
res.setBody('{"Href":"https://uatna11.springcm.com/v2/2371cf36-eb8a-43fe-9f28-b5bbe7644397/documentxmlmergetasks/TASK-123","Status":"Queued"}');
return res;
}
if (req.getMethod() == 'GET' && req.getEndpoint().contains('/documentxmlmergetasks/TASK-123')) {
res.setStatusCode(200);
res.setBody('{"Href":"https://uatna11.springcm.com/v2/2371cf36-eb8a-43fe-9f28-b5bbe7644397/documentxmlmergetasks/TASK-123","Status":"Completed"}');
return res;
}
if (req.getMethod() == 'GET' && req.getEndpoint().contains('/documents/template-guid')) {
res.setStatusCode(200);
res.setHeader('Content-Type', 'application/pdf');
res.setHeader('Content-Disposition', 'attachment; filename="GeneratedReview.pdf"');
res.setBodyAsBlob(Blob.valueOf('pdf-bytes'));
return res;
}
res.setStatusCode(404);
res.setBody('{"Message":"Not Found"}');
return res;
}
}
@IsTest
static void generatesDocumentAndPollsTaskStatus() {
Appraiser_Case__c appraiserCase = new Appraiser_Case__c(
Appraiser_Field_Review_Date__c = Date.newInstance(2026, 4, 2),
Letter_Sent_Date__c = Date.newInstance(2026, 4, 9),
FHA_Case_Number__c = '123-4567890',
Appraiser_Name__c = 'Jamie Appraiser',
Appraiser_Last_Name__c = 'Appraiser',
Property_Street__c = '123 Main & Main <Suite 5>',
Property_City__c = 'Denver',
Property_State_Province__c = 'CO',
Property_Postal_Code__c = '80202'
);
insert appraiserCase;
insert new Appraiser_Case_Deficiency__c(
Appraiser_Case__c = appraiserCase.Id,
Deficiency_Number__c = 1,
Description__c = 'Missing comparable sale adjustment detail.',
Resolution__c = 'Added supporting calculations & notes.',
Reference__c = 'VC-1'
);
Test.setMock(HttpCalloutMock.class, new CLMCalloutMock());
Test.startTest();
CLMDocGenCallout.CLMDocGenResponse response = CLMDocGenCallout.generateDocument(
appraiserCase.Id,
'https://uatna11.springcm.com/v2/2371cf36-eb8a-43fe-9f28-b5bbe7644397/documents/template-guid',
'https://uatna11.springcm.com/v2/2371cf36-eb8a-43fe-9f28-b5bbe7644397/folders/folder-guid',
'Review_AC-00001.docx',
'UAT'
);
CLMDocGenCallout.CLMDocGenResponse taskStatus = CLMDocGenCallout.getTaskStatus('TASK-123', 'S1');
Test.stopTest();
System.assertEquals(true, response.success);
System.assertEquals('TASK-123', response.documentId);
System.assert(response.documentUrl.contains('/documentxmlmergetasks/TASK-123'));
System.assertEquals(true, taskStatus.success);
System.assertEquals('Task status: Completed', taskStatus.message);
}
@IsTest
static void normalizesResourcePathsAndExtractsAccountIds() {
System.assertEquals(
'2371cf36-eb8a-43fe-9f28-b5bbe7644397',
CLMDocGenCallout.extractAccountId('https://uatna11.springcm.com/v2/2371cf36-eb8a-43fe-9f28-b5bbe7644397/folders/folder-guid')
);
System.assertEquals(
'/v2/2371cf36-eb8a-43fe-9f28-b5bbe7644397/folders/folder-guid',
CLMDocGenCallout.normalizeResourcePath(
'https://uatna11.springcm.com/v2/2371cf36-eb8a-43fe-9f28-b5bbe7644397/folders/folder-guid',
'UAT'
)
);
System.assertEquals(
'/v2/' + CLMDocGenCallout.defaultAccountId('UAT') + '/folders/folder-guid',
CLMDocGenCallout.normalizeResourcePath('/folders/folder-guid', 'UAT')
);
}
@IsTest
static void downloadsGeneratedDocumentThroughDownloadCredential() {
Test.setMock(HttpCalloutMock.class, new CLMCalloutMock());
Test.startTest();
CLMDocGenCallout.DownloadedDocument downloaded = CLMDocGenCallout.downloadDocument(
'https://apiuatna11.springcm.com/v2/bccae332-c7db-4892-ab85-257df0f70fea/documents/template-guid',
'UAT'
);
Test.stopTest();
System.assertEquals('GeneratedReview.pdf', downloaded.fileName);
System.assertEquals('application/pdf', downloaded.contentType);
System.assertNotEquals(null, downloaded.body);
}
@IsTest
static void buildsPreviewXmlAndRequestBody() {
Appraiser_Case__c appraiserCase = new Appraiser_Case__c(
Appraiser_Field_Review_Date__c = Date.newInstance(2026, 4, 2),
Letter_Sent_Date__c = Date.newInstance(2026, 4, 9),
FHA_Case_Number__c = '123-4567890',
Appraiser_Name__c = 'Jamie Appraiser',
Appraiser_Last_Name__c = 'Appraiser',
Property_Street__c = '245 Lexington Ave',
Property_City__c = 'New York',
Property_State_Province__c = 'NY',
Property_Postal_Code__c = '10016',
Property_Country__c = 'USA'
);
insert appraiserCase;
insert new Appraiser_Case_Deficiency__c(
Appraiser_Case__c = appraiserCase.Id,
Deficiency_Number__c = 1,
Description__c = 'Missing comparable sale adjustment detail.',
Resolution__c = 'Added supporting calculations.',
Reference__c = 'VC-1'
);
String dataXml = CLMDocGenCallout.buildDataXmlForCase(appraiserCase.Id);
String requestBodyJson = CLMDocGenCallout.buildRequestBodyJson(
appraiserCase.Id,
'https://apiuatna11.springcm.com/v2/bccae332-c7db-4892-ab85-257df0f70fea/documents/template-guid',
'https://apiuatna11.springcm.com/v2/bccae332-c7db-4892-ab85-257df0f70fea/folders/folder-guid',
'Review_AC-000002.docx'
);
System.assert(dataXml.contains('<LetterSentDate>'));
System.assert(dataXml.contains('<FHACaseNumber>123-4567890</FHACaseNumber>'));
System.assert(dataXml.contains('<Reference>VC-1</Reference>'));
System.assert(requestBodyJson.contains('"DataXML"'));
System.assert(requestBodyJson.contains('Review_AC-000002.docx'));
System.assert(requestBodyJson.contains('template-guid'));
}
}

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>63.0</apiVersion>
<status>Active</status>
</ApexClass>

View File

@ -0,0 +1,339 @@
public with sharing class DocusignESignatureService {
public class ESignatureAccountConfig {
@AuraEnabled public String accountCode;
@AuraEnabled public String accountDisplayName;
@AuraEnabled public String environment;
@AuraEnabled public String eSignatureAuthNamedCredential;
@AuraEnabled public String eSignatureRestNamedCredential;
@AuraEnabled public String eSignatureAccountId;
}
public class ApiResponse {
@AuraEnabled public Boolean success;
@AuraEnabled public Integer statusCode;
@AuraEnabled public String message;
@AuraEnabled public String requestPath;
@AuraEnabled public String responseBody;
}
public class ESignatureAccountSummary {
@AuraEnabled public String accountId;
@AuraEnabled public String accountName;
@AuraEnabled public String baseUri;
@AuraEnabled public Boolean isDefault;
@AuraEnabled public String rawJson;
}
public class TemplateSummary {
@AuraEnabled public String templateId;
@AuraEnabled public String name;
@AuraEnabled public String description;
@AuraEnabled public String shared;
@AuraEnabled public String lastModified;
@AuraEnabled public String rawJson;
}
public class EnvelopeSummary {
@AuraEnabled public String envelopeId;
@AuraEnabled public String emailSubject;
@AuraEnabled public String status;
@AuraEnabled public String createdDateTime;
@AuraEnabled public String sentDateTime;
@AuraEnabled public String completedDateTime;
@AuraEnabled public String rawJson;
}
@AuraEnabled(cacheable=true)
public static ESignatureAccountConfig getAccountConfig(String accountCode) {
CLM_Account_Setting__mdt row = requireAccountSetting(accountCode);
ESignatureAccountConfig config = new ESignatureAccountConfig();
config.accountCode = String.isNotBlank(row.Account_Code__c) ? row.Account_Code__c : row.DeveloperName;
config.accountDisplayName = String.isNotBlank(row.Account_Display_Name__c) ? row.Account_Display_Name__c : row.DeveloperName;
config.environment = row.Environment_Code__c;
config.eSignatureAuthNamedCredential = row.ESignature_Auth_Named_Credential__c;
config.eSignatureRestNamedCredential = row.ESignature_Rest_Named_Credential__c;
config.eSignatureAccountId = row.ESignature_Account_Id__c;
return config;
}
@AuraEnabled(cacheable=false)
public static ApiResponse probe(String accountCode, String relativePath) {
CLM_Account_Setting__mdt row = requireAccountSetting(accountCode);
String normalizedPath = normalizePath(relativePath);
HttpRequest req = new HttpRequest();
req.setEndpoint(buildEndpoint(normalizedPath, row.ESignature_Rest_Named_Credential__c));
req.setMethod('GET');
req.setTimeout(30000);
HttpResponse res = new Http().send(req);
ApiResponse response = new ApiResponse();
response.success = res.getStatusCode() >= 200 && res.getStatusCode() < 300;
response.statusCode = res.getStatusCode();
response.message = response.success ? 'eSignature request succeeded.' : 'eSignature request failed.';
response.requestPath = normalizedPath;
response.responseBody = res.getBody();
return response;
}
@AuraEnabled(cacheable=false)
public static List<ESignatureAccountSummary> listAccounts(String accountCode) {
ApiResponse response = getLoginInformation(accountCode);
if (!response.success) {
throw new AuraHandledException('eSignature API Error (HTTP ' + response.statusCode + '): ' + response.responseBody);
}
return parseAccountList(response.responseBody);
}
@AuraEnabled(cacheable=false)
public static ApiResponse getLoginInformation(String accountCode) {
return probe(accountCode, '/v2.1/login_information');
}
@AuraEnabled(cacheable=false)
public static ApiResponse getUserInfo(String accountCode) {
CLM_Account_Setting__mdt row = requireAccountSetting(accountCode);
HttpRequest req = new HttpRequest();
req.setEndpoint(buildEndpoint('/oauth/userinfo', authNamedCredential(row)));
req.setMethod('GET');
req.setTimeout(30000);
HttpResponse res = new Http().send(req);
ApiResponse response = new ApiResponse();
response.success = res.getStatusCode() >= 200 && res.getStatusCode() < 300;
response.statusCode = res.getStatusCode();
response.message = response.success ? 'eSignature user info request succeeded.' : 'eSignature user info request failed.';
response.requestPath = '/oauth/userinfo';
response.responseBody = res.getBody();
return response;
}
@AuraEnabled(cacheable=false)
public static ApiResponse getAccountInformation(String accountCode, String eSignatureAccountId) {
CLM_Account_Setting__mdt row = requireAccountSetting(accountCode);
String targetAccountId = requireESignatureAccountId(row, accountCode, eSignatureAccountId);
return probe(accountCode, '/v2.1/accounts/' + targetAccountId);
}
@AuraEnabled(cacheable=false)
public static List<TemplateSummary> listTemplates(String accountCode) {
CLM_Account_Setting__mdt row = requireAccountSetting(accountCode);
String targetAccountId = requireESignatureAccountId(row, accountCode, null);
ApiResponse response = probe(accountCode, '/v2.1/accounts/' + targetAccountId + '/templates');
if (!response.success) {
throw new AuraHandledException('eSignature API Error (HTTP ' + response.statusCode + '): ' + response.responseBody);
}
return parseTemplateList(response.responseBody);
}
@AuraEnabled(cacheable=false)
public static List<EnvelopeSummary> listEnvelopes(String accountCode, String fromDate) {
CLM_Account_Setting__mdt row = requireAccountSetting(accountCode);
String targetAccountId = requireESignatureAccountId(row, accountCode, null);
String normalizedFromDate = String.isBlank(fromDate)
? DateTime.newInstanceGMT(Date.today().addDays(-30), Time.newInstance(0, 0, 0, 0)).formatGMT('yyyy-MM-dd')
: fromDate.trim();
ApiResponse response = probe(
accountCode,
'/v2.1/accounts/' + targetAccountId + '/envelopes?from_date=' + EncodingUtil.urlEncode(normalizedFromDate, 'UTF-8')
);
if (!response.success) {
throw new AuraHandledException('eSignature API Error (HTTP ' + response.statusCode + '): ' + response.responseBody);
}
return parseEnvelopeList(response.responseBody);
}
@TestVisible
private static List<ESignatureAccountSummary> parseAccountList(String body) {
Object root = JSON.deserializeUntyped(body);
List<Object> records = new List<Object>();
if (root instanceof List<Object>) {
records = (List<Object>) root;
} else if (root instanceof Map<String, Object>) {
Object loginAccounts = ((Map<String, Object>) root).get('loginAccounts');
if (loginAccounts instanceof List<Object>) {
records = (List<Object>) loginAccounts;
}
Object accounts = ((Map<String, Object>) root).get('accounts');
if (records.isEmpty() && accounts instanceof List<Object>) {
records = (List<Object>) accounts;
}
}
List<ESignatureAccountSummary> summaries = new List<ESignatureAccountSummary>();
for (Object record : records) {
if (!(record instanceof Map<String, Object>)) {
continue;
}
Map<String, Object> row = (Map<String, Object>) record;
ESignatureAccountSummary summary = new ESignatureAccountSummary();
summary.accountId = firstString(row, new List<String>{ 'accountId', 'account_id' });
summary.accountName = firstString(row, new List<String>{ 'accountName', 'account_name', 'name' });
summary.baseUri = firstString(row, new List<String>{ 'baseUri', 'base_uri', 'baseUrl', 'base_url' });
summary.isDefault = parseBoolean(row.get('isDefault'));
summary.rawJson = JSON.serialize(row);
summaries.add(summary);
}
return summaries;
}
@TestVisible
private static List<TemplateSummary> parseTemplateList(String body) {
List<Object> records = extractCollection(body, new List<String>{ 'envelopeTemplates', 'templates' });
List<TemplateSummary> summaries = new List<TemplateSummary>();
for (Object record : records) {
if (!(record instanceof Map<String, Object>)) {
continue;
}
Map<String, Object> row = (Map<String, Object>) record;
TemplateSummary summary = new TemplateSummary();
summary.templateId = firstString(row, new List<String>{ 'templateId', 'template_id' });
summary.name = firstString(row, new List<String>{ 'name' });
summary.description = firstString(row, new List<String>{ 'description' });
summary.shared = firstString(row, new List<String>{ 'shared' });
summary.lastModified = firstString(row, new List<String>{ 'lastModified', 'last_modified', 'lastModifiedDateTime' });
summary.rawJson = JSON.serialize(row);
summaries.add(summary);
}
return summaries;
}
@TestVisible
private static List<EnvelopeSummary> parseEnvelopeList(String body) {
List<Object> records = extractCollection(body, new List<String>{ 'envelopes' });
List<EnvelopeSummary> summaries = new List<EnvelopeSummary>();
for (Object record : records) {
if (!(record instanceof Map<String, Object>)) {
continue;
}
Map<String, Object> row = (Map<String, Object>) record;
EnvelopeSummary summary = new EnvelopeSummary();
summary.envelopeId = firstString(row, new List<String>{ 'envelopeId', 'envelope_id' });
summary.emailSubject = firstString(row, new List<String>{ 'emailSubject', 'email_subject' });
summary.status = firstString(row, new List<String>{ 'status' });
summary.createdDateTime = firstString(row, new List<String>{ 'createdDateTime', 'created_datetime' });
summary.sentDateTime = firstString(row, new List<String>{ 'sentDateTime', 'sent_datetime' });
summary.completedDateTime = firstString(row, new List<String>{ 'completedDateTime', 'completed_datetime' });
summary.rawJson = JSON.serialize(row);
summaries.add(summary);
}
return summaries;
}
@TestVisible
private static String buildEndpoint(String relativePath, String namedCredential) {
if (String.isBlank(namedCredential)) {
throw new AuraHandledException('No eSignature named credential is configured for this account.');
}
return 'callout:' + namedCredential + normalizePath(relativePath);
}
private static String normalizePath(String relativePath) {
if (String.isBlank(relativePath)) {
throw new AuraHandledException('A relative path is required.');
}
return relativePath.startsWith('/') ? relativePath : '/' + relativePath;
}
private static CLM_Account_Setting__mdt requireAccountSetting(String accountCode) {
String normalizedCode = String.isBlank(accountCode) ? null : accountCode.trim();
List<CLM_Account_Setting__mdt> rows = new List<CLM_Account_Setting__mdt>();
if (String.isNotBlank(normalizedCode)) {
rows = [
SELECT DeveloperName,
Account_Code__c,
Account_Display_Name__c,
Environment_Code__c,
ESignature_Auth_Named_Credential__c,
ESignature_Rest_Named_Credential__c,
ESignature_Account_Id__c,
Active__c
FROM CLM_Account_Setting__mdt
WHERE Active__c = true
AND DeveloperName = :normalizedCode
LIMIT 1
];
if (rows.isEmpty()) {
rows = [
SELECT DeveloperName,
Account_Code__c,
Account_Display_Name__c,
Environment_Code__c,
ESignature_Auth_Named_Credential__c,
ESignature_Rest_Named_Credential__c,
ESignature_Account_Id__c,
Active__c
FROM CLM_Account_Setting__mdt
WHERE Active__c = true
AND Account_Code__c = :normalizedCode
LIMIT 1
];
}
}
if (rows.isEmpty()) {
throw new AuraHandledException('No active CLM account setting was found for ' + accountCode + '.');
}
CLM_Account_Setting__mdt row = rows[0];
if (String.isBlank(row.ESignature_Rest_Named_Credential__c)) {
throw new AuraHandledException('No eSignature named credential is configured for ' + accountCode + '.');
}
return row;
}
private static String authNamedCredential(CLM_Account_Setting__mdt row) {
return String.isNotBlank(row.ESignature_Auth_Named_Credential__c)
? row.ESignature_Auth_Named_Credential__c
: row.ESignature_Rest_Named_Credential__c;
}
private static String requireESignatureAccountId(CLM_Account_Setting__mdt row, String accountCode, String eSignatureAccountId) {
String targetAccountId = String.isNotBlank(eSignatureAccountId) ? eSignatureAccountId : row.ESignature_Account_Id__c;
if (String.isBlank(targetAccountId)) {
throw new AuraHandledException('No eSignature account id is configured for ' + accountCode + '.');
}
return targetAccountId;
}
private static List<Object> extractCollection(String body, List<String> keys) {
Object root = JSON.deserializeUntyped(body);
if (root instanceof List<Object>) {
return (List<Object>) root;
}
if (root instanceof Map<String, Object>) {
Map<String, Object> source = (Map<String, Object>) root;
for (String key : keys) {
Object records = source.get(key);
if (records instanceof List<Object>) {
return (List<Object>) records;
}
}
}
return new List<Object>();
}
private static String firstString(Map<String, Object> source, List<String> keys) {
for (String key : keys) {
Object value = source.get(key);
if (value != null) {
String text = String.valueOf(value);
if (String.isNotBlank(text)) {
return text;
}
}
}
return null;
}
private static Boolean parseBoolean(Object value) {
if (value == null) {
return false;
}
if (value instanceof Boolean) {
return (Boolean) value;
}
return String.valueOf(value).toLowerCase() == 'true';
}
}

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>63.0</apiVersion>
<status>Active</status>
</ApexClass>

View File

@ -0,0 +1,150 @@
@IsTest
private class DocusignESignatureServiceTest {
private class ESignatureMock implements HttpCalloutMock {
public HttpResponse respond(HttpRequest req) {
HttpResponse res = new HttpResponse();
res.setHeader('Content-Type', 'application/json');
if (req.getMethod() == 'GET' && req.getEndpoint().contains('/oauth/userinfo')) {
res.setStatusCode(200);
res.setBody('{"sub":"d9aab149-ff54-408c-a748-baa4b56e2fcd","accounts":[{"account_id":"12345678","account_name":"Demo eSignature Account","base_uri":"https://demo.docusign.net","is_default":true}]}');
return res;
}
if (req.getMethod() == 'GET' && req.getEndpoint().contains('/templates')) {
res.setStatusCode(200);
res.setBody('{"envelopeTemplates":[{"templateId":"tmpl-001","name":"Review Letter","description":"Appraiser review letter template","shared":"true","lastModified":"2026-04-08T12:00:00Z"},{"templateId":"tmpl-002","name":"Alternate Review Letter","description":"Alternate version","shared":"false","lastModified":"2026-04-07T12:00:00Z"}]}');
return res;
}
if (req.getMethod() == 'GET' && req.getEndpoint().contains('/envelopes?from_date=')) {
res.setStatusCode(200);
res.setBody('{"envelopes":[{"envelopeId":"env-001","emailSubject":"Appraiser Review","status":"completed","createdDateTime":"2026-04-01T10:00:00Z","sentDateTime":"2026-04-01T10:05:00Z","completedDateTime":"2026-04-01T10:15:00Z"},{"envelopeId":"env-002","emailSubject":"Second Review","status":"sent","createdDateTime":"2026-04-02T09:00:00Z","sentDateTime":"2026-04-02T09:05:00Z"}]}');
return res;
}
if (req.getMethod() == 'GET' && req.getEndpoint().contains('/v2.1/accounts/12345678')) {
res.setStatusCode(200);
res.setBody('{"accountId":"12345678","accountName":"Demo eSignature Account","status":"active"}');
return res;
}
if (req.getMethod() == 'GET' && req.getEndpoint().contains('/v2.1/login_information')) {
res.setStatusCode(200);
res.setBody('{"loginAccounts":[{"accountId":"12345678","name":"Demo eSignature Account","baseUrl":"https://demo.docusign.net/restapi/v2.1/accounts/12345678","isDefault":"true"},{"accountId":"87654321","name":"Secondary Demo Account","baseUrl":"https://demo.docusign.net/restapi/v2.1/accounts/87654321","isDefault":"false"}]}');
return res;
}
res.setStatusCode(404);
res.setBody('{"message":"Not Found"}');
return res;
}
}
@IsTest
static void returnsConfiguredAccountInfo() {
Test.startTest();
DocusignESignatureService.ESignatureAccountConfig config = DocusignESignatureService.getAccountConfig('DTC_CLM_Demo');
Test.stopTest();
System.assertEquals('DTC_CLM_Demo', config.accountCode);
System.assertEquals('DTC CLM Demo', config.accountDisplayName);
System.assertEquals('AcctDemo_NamedCreds', config.eSignatureAuthNamedCredential);
System.assertEquals('Esignature_Demo_NamedCreds', config.eSignatureRestNamedCredential);
}
@IsTest
static void listsAccountsFromEsignatureApi() {
Test.setMock(HttpCalloutMock.class, new ESignatureMock());
Test.startTest();
List<DocusignESignatureService.ESignatureAccountSummary> accounts = DocusignESignatureService.listAccounts('DTC_CLM_Demo');
Test.stopTest();
System.assertEquals(2, accounts.size());
System.assertEquals('12345678', accounts[0].accountId);
System.assertEquals('Demo eSignature Account', accounts[0].accountName);
System.assertEquals('https://demo.docusign.net/restapi/v2.1/accounts/12345678', accounts[0].baseUri);
System.assertEquals(true, accounts[0].isDefault);
}
@IsTest
static void getsLoginInformationUsingWorkingEndpoint() {
Test.setMock(HttpCalloutMock.class, new ESignatureMock());
Test.startTest();
DocusignESignatureService.ApiResponse response = DocusignESignatureService.getLoginInformation('DTC_CLM_Demo');
Test.stopTest();
System.assertEquals(true, response.success);
System.assertEquals(200, response.statusCode);
System.assert(response.responseBody.contains('loginAccounts'));
}
@IsTest
static void getsUserInfoUsingAuthNamedCredential() {
Test.setMock(HttpCalloutMock.class, new ESignatureMock());
Test.startTest();
DocusignESignatureService.ApiResponse response = DocusignESignatureService.getUserInfo('DTC_CLM_Demo');
Test.stopTest();
System.assertEquals(true, response.success);
System.assertEquals(200, response.statusCode);
System.assertEquals('/oauth/userinfo', response.requestPath);
System.assert(response.responseBody.contains('"accounts"'));
}
@IsTest
static void getsAccountInformationUsingConfiguredAccountId() {
Test.setMock(HttpCalloutMock.class, new ESignatureMock());
Test.startTest();
DocusignESignatureService.ApiResponse response = DocusignESignatureService.getAccountInformation(
'DTC_CLM_Demo',
'12345678'
);
Test.stopTest();
System.assertEquals(true, response.success);
System.assertEquals(200, response.statusCode);
System.assert(response.responseBody.contains('Demo eSignature Account'));
}
@IsTest
static void listsTemplatesUsingConfiguredAccountId() {
Test.setMock(HttpCalloutMock.class, new ESignatureMock());
Test.startTest();
List<DocusignESignatureService.TemplateSummary> templates = DocusignESignatureService.listTemplates('DTC_CLM_Demo');
Test.stopTest();
System.assertEquals(2, templates.size());
System.assertEquals('tmpl-001', templates[0].templateId);
System.assertEquals('Review Letter', templates[0].name);
}
@IsTest
static void listsRecentEnvelopesUsingConfiguredAccountId() {
Test.setMock(HttpCalloutMock.class, new ESignatureMock());
Test.startTest();
List<DocusignESignatureService.EnvelopeSummary> envelopes = DocusignESignatureService.listEnvelopes(
'DTC_CLM_Demo',
'2026-04-01'
);
Test.stopTest();
System.assertEquals(2, envelopes.size());
System.assertEquals('env-001', envelopes[0].envelopeId);
System.assertEquals('completed', envelopes[0].status);
}
@IsTest
static void buildsEndpointWithNamedCredential() {
System.assertEquals(
'callout:Esignature_Demo_NamedCreds/v2.1/accounts',
DocusignESignatureService.buildEndpoint('/v2.1/accounts', 'Esignature_Demo_NamedCreds')
);
}
}

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>63.0</apiVersion>
<status>Active</status>
</ApexClass>

View File

@ -0,0 +1,57 @@
<?xml version="1.0" encoding="UTF-8"?>
<CustomMetadata xmlns="http://soap.sforce.com/2006/04/metadata">
<label>DTC CLM Demo</label>
<protected>false</protected>
<values>
<field>Account_Code__c</field>
<value xsi:type="xsd:string" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">DTC_CLM_Demo</value>
</values>
<values>
<field>Account_Display_Name__c</field>
<value xsi:type="xsd:string" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">DTC CLM Demo</value>
</values>
<values>
<field>Environment_Code__c</field>
<value xsi:type="xsd:string" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">UAT</value>
</values>
<values>
<field>CLM_Account_Id__c</field>
<value xsi:type="xsd:string" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">bccae332-c7db-4892-ab85-257df0f70fea</value>
</values>
<values>
<field>CLM_Api_Named_Credential__c</field>
<value xsi:type="xsd:string" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">CLMuatNamedCreds</value>
</values>
<values>
<field>CLM_Download_Named_Credential__c</field>
<value xsi:type="xsd:string" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">CLMuatDownload</value>
</values>
<values>
<field>ESignature_Auth_Named_Credential__c</field>
<value xsi:type="xsd:string" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">AcctDemo_NamedCreds</value>
</values>
<values>
<field>ESignature_Rest_Named_Credential__c</field>
<value xsi:type="xsd:string" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">Esignature_Demo_NamedCreds</value>
</values>
<values>
<field>Template_Root_Folder_Href__c</field>
<value xsi:type="xsd:string" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">https://apiuatna11.springcm.com/v2/bccae332-c7db-4892-ab85-257df0f70fea/folders/12220442-b12e-f111-84fc-88e9a4bd0d9c</value>
</values>
<values>
<field>Destination_Root_Folder_Href__c</field>
<value xsi:type="xsd:string" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">https://apiuatna11.springcm.com/v2/bccae332-c7db-4892-ab85-257df0f70fea/folders/12220442-b12e-f111-84fc-88e9a4bd0d9c</value>
</values>
<values>
<field>Default_Template_Document_Href__c</field>
<value xsi:type="xsd:string" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">https://apiuatna11.springcm.com/v2/bccae332-c7db-4892-ab85-257df0f70fea/documents/a0cbc0e6-d87d-459e-8d63-66baa47878f3</value>
</values>
<values>
<field>Default_Destination_Document_Name_Prefix__c</field>
<value xsi:type="xsd:string" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">Review</value>
</values>
<values>
<field>Active__c</field>
<value xsi:type="xsd:boolean" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">true</value>
</values>
</CustomMetadata>

View File

@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8"?>
<CustomMetadata xmlns="http://soap.sforce.com/2006/04/metadata">
<label>DTC HUD Demo</label>
<protected>false</protected>
<values>
<field>Account_Code__c</field>
<value xsi:type="xsd:string" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">DTC_HUD_Demo</value>
</values>
<values>
<field>Account_Display_Name__c</field>
<value xsi:type="xsd:string" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">DTC HUD Demo</value>
</values>
<values>
<field>Environment_Code__c</field>
<value xsi:type="xsd:string" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">S1</value>
</values>
<values>
<field>CLM_Account_Id__c</field>
<value xsi:type="xsd:string" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">2371cf36-eb8a-43fe-9f28-b5bbe7644397</value>
</values>
<values>
<field>CLM_Api_Named_Credential__c</field>
<value xsi:type="xsd:string" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">CLMs1NamedCreds</value>
</values>
<values>
<field>CLM_Download_Named_Credential__c</field>
<value xsi:type="xsd:string" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">CLMs1Download</value>
</values>
<values>
<field>ESignature_Auth_Named_Credential__c</field>
<value xsi:type="xsd:string" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">AcctDemo_NamedCreds</value>
</values>
<values>
<field>ESignature_Rest_Named_Credential__c</field>
<value xsi:type="xsd:string" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">Esignature_Demo_NamedCreds</value>
</values>
<values>
<field>Default_Destination_Document_Name_Prefix__c</field>
<value xsi:type="xsd:string" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">Review</value>
</values>
<values>
<field>Active__c</field>
<value xsi:type="xsd:boolean" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">true</value>
</values>
</CustomMetadata>

View File

@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8"?>
<CustomMetadata xmlns="http://soap.sforce.com/2006/04/metadata">
<label>DTC IAM Enterprise</label>
<protected>false</protected>
<values>
<field>Account_Code__c</field>
<value xsi:type="xsd:string" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">DTC_IAM_Enterprise</value>
</values>
<values>
<field>Account_Display_Name__c</field>
<value xsi:type="xsd:string" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">DTC IAM Enterprise</value>
</values>
<values>
<field>Environment_Code__c</field>
<value xsi:type="xsd:string" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">S1</value>
</values>
<values>
<field>CLM_Account_Id__c</field>
<value xsi:type="xsd:string" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">2371cf36-eb8a-43fe-9f28-b5bbe7644397</value>
</values>
<values>
<field>CLM_Api_Named_Credential__c</field>
<value xsi:type="xsd:string" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">CLMs1NamedCreds</value>
</values>
<values>
<field>CLM_Download_Named_Credential__c</field>
<value xsi:type="xsd:string" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">CLMs1Download</value>
</values>
<values>
<field>ESignature_Auth_Named_Credential__c</field>
<value xsi:type="xsd:string" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">AcctDemo_NamedCreds</value>
</values>
<values>
<field>ESignature_Rest_Named_Credential__c</field>
<value xsi:type="xsd:string" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">Esignature_Demo_NamedCreds</value>
</values>
<values>
<field>Default_Destination_Document_Name_Prefix__c</field>
<value xsi:type="xsd:string" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">Review</value>
</values>
<values>
<field>Active__c</field>
<value xsi:type="xsd:boolean" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">true</value>
</values>
</CustomMetadata>

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<CustomMetadata xmlns="http://soap.sforce.com/2006/04/metadata">
<label>S1</label>
<protected>false</protected>
<values>
<field>Environment_Code__c</field>
<value xsi:type="xsd:string" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">S1</value>
</values>
<values>
<field>Default_Destination_Document_Name_Prefix__c</field>
<value xsi:type="xsd:string" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">Review</value>
</values>
</CustomMetadata>

View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<CustomMetadata xmlns="http://soap.sforce.com/2006/04/metadata">
<label>UAT</label>
<protected>false</protected>
<values>
<field>Environment_Code__c</field>
<value xsi:type="xsd:string" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">UAT</value>
</values>
<values>
<field>Destination_Root_Folder_Href__c</field>
<value xsi:type="xsd:string" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">https://apiuatna11.springcm.com/v2/bccae332-c7db-4892-ab85-257df0f70fea/folders/12220442-b12e-f111-84fc-88e9a4bd0d9c</value>
</values>
<values>
<field>Template_Root_Folder_Href__c</field>
<value xsi:type="xsd:string" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">https://apiuatna11.springcm.com/v2/bccae332-c7db-4892-ab85-257df0f70fea/folders/12220442-b12e-f111-84fc-88e9a4bd0d9c</value>
</values>
<values>
<field>Default_Template_Document_Href__c</field>
<value xsi:type="xsd:string" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">https://apiuatna11.springcm.com/v2/bccae332-c7db-4892-ab85-257df0f70fea/documents/a0cbc0e6-d87d-459e-8d63-66baa47878f3</value>
</values>
<values>
<field>Default_Destination_Document_Name_Prefix__c</field>
<value xsi:type="xsd:string" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">Review</value>
</values>
</CustomMetadata>

View File

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8"?>
<CustomMetadata xmlns="http://soap.sforce.com/2006/04/metadata">
<label>Appraiser Review Letter</label>
<protected>false</protected>
<values>
<field>Account_Code__c</field>
<value xsi:type="xsd:string" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">DTC_CLM_Demo</value>
</values>
<values>
<field>Letter_Code__c</field>
<value xsi:type="xsd:string" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">APPRAISER_REVIEW</value>
</values>
<values>
<field>Letter_Display_Name__c</field>
<value xsi:type="xsd:string" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">Appraiser Review Letter</value>
</values>
<values>
<field>Description__c</field>
<value xsi:type="xsd:string" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">Current appraiser letter template flow.</value>
</values>
<values>
<field>Active__c</field>
<value xsi:type="xsd:boolean" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">true</value>
</values>
<values>
<field>Is_Default__c</field>
<value xsi:type="xsd:boolean" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">true</value>
</values>
<values>
<field>Sort_Order__c</field>
<value xsi:type="xsd:double" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">10</value>
</values>
<values>
<field>Default_Destination_Document_Name_Prefix__c</field>
<value xsi:type="xsd:string" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">Review</value>
</values>
</CustomMetadata>

View File

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8"?>
<CustomMetadata xmlns="http://soap.sforce.com/2006/04/metadata">
<label>Education Letter</label>
<protected>false</protected>
<values>
<field>Account_Code__c</field>
<value xsi:type="xsd:string" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">DTC_CLM_Demo</value>
</values>
<values>
<field>Letter_Code__c</field>
<value xsi:type="xsd:string" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">EDUCATION_LETTER</value>
</values>
<values>
<field>Letter_Display_Name__c</field>
<value xsi:type="xsd:string" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">Education Letter</value>
</values>
<values>
<field>Description__c</field>
<value xsi:type="xsd:string" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">Education / guidance letter.</value>
</values>
<values>
<field>Active__c</field>
<value xsi:type="xsd:boolean" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">true</value>
</values>
<values>
<field>Is_Default__c</field>
<value xsi:type="xsd:boolean" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">false</value>
</values>
<values>
<field>Sort_Order__c</field>
<value xsi:type="xsd:double" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">40</value>
</values>
<values>
<field>Default_Destination_Document_Name_Prefix__c</field>
<value xsi:type="xsd:string" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">Education</value>
</values>
</CustomMetadata>

View File

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8"?>
<CustomMetadata xmlns="http://soap.sforce.com/2006/04/metadata">
<label>Intent to Remove Letter</label>
<protected>false</protected>
<values>
<field>Account_Code__c</field>
<value xsi:type="xsd:string" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">DTC_CLM_Demo</value>
</values>
<values>
<field>Letter_Code__c</field>
<value xsi:type="xsd:string" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">INTENT_TO_REMOVE_LETTER</value>
</values>
<values>
<field>Letter_Display_Name__c</field>
<value xsi:type="xsd:string" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">Intent to Remove Letter</value>
</values>
<values>
<field>Description__c</field>
<value xsi:type="xsd:string" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">Intent to Remove letter.</value>
</values>
<values>
<field>Active__c</field>
<value xsi:type="xsd:boolean" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">true</value>
</values>
<values>
<field>Is_Default__c</field>
<value xsi:type="xsd:boolean" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">false</value>
</values>
<values>
<field>Sort_Order__c</field>
<value xsi:type="xsd:double" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">30</value>
</values>
<values>
<field>Default_Destination_Document_Name_Prefix__c</field>
<value xsi:type="xsd:string" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">Intent_Remove</value>
</values>
</CustomMetadata>

View File

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8"?>
<CustomMetadata xmlns="http://soap.sforce.com/2006/04/metadata">
<label>NOD Letter</label>
<protected>false</protected>
<values>
<field>Account_Code__c</field>
<value xsi:type="xsd:string" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">DTC_CLM_Demo</value>
</values>
<values>
<field>Letter_Code__c</field>
<value xsi:type="xsd:string" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">NOD_LETTER</value>
</values>
<values>
<field>Letter_Display_Name__c</field>
<value xsi:type="xsd:string" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">NOD Letter</value>
</values>
<values>
<field>Description__c</field>
<value xsi:type="xsd:string" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">Notice of Deficiency letter.</value>
</values>
<values>
<field>Active__c</field>
<value xsi:type="xsd:boolean" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">true</value>
</values>
<values>
<field>Is_Default__c</field>
<value xsi:type="xsd:boolean" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">false</value>
</values>
<values>
<field>Sort_Order__c</field>
<value xsi:type="xsd:double" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">20</value>
</values>
<values>
<field>Default_Destination_Document_Name_Prefix__c</field>
<value xsi:type="xsd:string" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">NOD</value>
</values>
</CustomMetadata>

View File

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8"?>
<CustomMetadata xmlns="http://soap.sforce.com/2006/04/metadata">
<label>Appraiser Review Letter</label>
<protected>false</protected>
<values>
<field>Account_Code__c</field>
<value xsi:type="xsd:string" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">DTC_HUD_Demo</value>
</values>
<values>
<field>Letter_Code__c</field>
<value xsi:type="xsd:string" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">APPRAISER_REVIEW</value>
</values>
<values>
<field>Letter_Display_Name__c</field>
<value xsi:type="xsd:string" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">Appraiser Review Letter</value>
</values>
<values>
<field>Description__c</field>
<value xsi:type="xsd:string" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">Current appraiser letter template flow.</value>
</values>
<values>
<field>Active__c</field>
<value xsi:type="xsd:boolean" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">true</value>
</values>
<values>
<field>Is_Default__c</field>
<value xsi:type="xsd:boolean" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">true</value>
</values>
<values>
<field>Sort_Order__c</field>
<value xsi:type="xsd:double" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">10</value>
</values>
<values>
<field>Default_Destination_Document_Name_Prefix__c</field>
<value xsi:type="xsd:string" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">Review</value>
</values>
</CustomMetadata>

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<CustomMetadata xmlns="http://soap.sforce.com/2006/04/metadata">
<label>Education Letter</label>
<protected>false</protected>
<values><field>Account_Code__c</field><value xsi:type="xsd:string" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">DTC_HUD_Demo</value></values>
<values><field>Letter_Code__c</field><value xsi:type="xsd:string" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">EDUCATION_LETTER</value></values>
<values><field>Letter_Display_Name__c</field><value xsi:type="xsd:string" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">Education Letter</value></values>
<values><field>Description__c</field><value xsi:type="xsd:string" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">Education / guidance letter.</value></values>
<values><field>Active__c</field><value xsi:type="xsd:boolean" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">true</value></values>
<values><field>Is_Default__c</field><value xsi:type="xsd:boolean" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">false</value></values>
<values><field>Sort_Order__c</field><value xsi:type="xsd:double" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">40</value></values>
<values><field>Default_Destination_Document_Name_Prefix__c</field><value xsi:type="xsd:string" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">Education</value></values>
</CustomMetadata>

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<CustomMetadata xmlns="http://soap.sforce.com/2006/04/metadata">
<label>Intent to Remove Letter</label>
<protected>false</protected>
<values><field>Account_Code__c</field><value xsi:type="xsd:string" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">DTC_HUD_Demo</value></values>
<values><field>Letter_Code__c</field><value xsi:type="xsd:string" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">INTENT_TO_REMOVE_LETTER</value></values>
<values><field>Letter_Display_Name__c</field><value xsi:type="xsd:string" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">Intent to Remove Letter</value></values>
<values><field>Description__c</field><value xsi:type="xsd:string" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">Intent to Remove letter.</value></values>
<values><field>Active__c</field><value xsi:type="xsd:boolean" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">true</value></values>
<values><field>Is_Default__c</field><value xsi:type="xsd:boolean" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">false</value></values>
<values><field>Sort_Order__c</field><value xsi:type="xsd:double" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">30</value></values>
<values><field>Default_Destination_Document_Name_Prefix__c</field><value xsi:type="xsd:string" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">Intent_Remove</value></values>
</CustomMetadata>

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<CustomMetadata xmlns="http://soap.sforce.com/2006/04/metadata">
<label>NOD Letter</label>
<protected>false</protected>
<values><field>Account_Code__c</field><value xsi:type="xsd:string" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">DTC_HUD_Demo</value></values>
<values><field>Letter_Code__c</field><value xsi:type="xsd:string" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">NOD_LETTER</value></values>
<values><field>Letter_Display_Name__c</field><value xsi:type="xsd:string" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">NOD Letter</value></values>
<values><field>Description__c</field><value xsi:type="xsd:string" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">Notice of Deficiency letter.</value></values>
<values><field>Active__c</field><value xsi:type="xsd:boolean" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">true</value></values>
<values><field>Is_Default__c</field><value xsi:type="xsd:boolean" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">false</value></values>
<values><field>Sort_Order__c</field><value xsi:type="xsd:double" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">20</value></values>
<values><field>Default_Destination_Document_Name_Prefix__c</field><value xsi:type="xsd:string" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">NOD</value></values>
</CustomMetadata>

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<CustomMetadata xmlns="http://soap.sforce.com/2006/04/metadata">
<label>Intent to Remove Letter</label>
<protected>false</protected>
<values><field>Account_Code__c</field><value xsi:type="xsd:string" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">DTC_IAM_Enterprise</value></values>
<values><field>Letter_Code__c</field><value xsi:type="xsd:string" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">INTENT_TO_REMOVE_LETTER</value></values>
<values><field>Letter_Display_Name__c</field><value xsi:type="xsd:string" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">Intent to Remove Letter</value></values>
<values><field>Description__c</field><value xsi:type="xsd:string" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">Intent to Remove letter.</value></values>
<values><field>Active__c</field><value xsi:type="xsd:boolean" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">true</value></values>
<values><field>Is_Default__c</field><value xsi:type="xsd:boolean" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">false</value></values>
<values><field>Sort_Order__c</field><value xsi:type="xsd:double" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">30</value></values>
<values><field>Default_Destination_Document_Name_Prefix__c</field><value xsi:type="xsd:string" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">Intent_Remove</value></values>
</CustomMetadata>

View File

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8"?>
<CustomMetadata xmlns="http://soap.sforce.com/2006/04/metadata">
<label>Appraiser Review Letter</label>
<protected>false</protected>
<values>
<field>Account_Code__c</field>
<value xsi:type="xsd:string" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">DTC_IAM_Enterprise</value>
</values>
<values>
<field>Letter_Code__c</field>
<value xsi:type="xsd:string" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">APPRAISER_REVIEW</value>
</values>
<values>
<field>Letter_Display_Name__c</field>
<value xsi:type="xsd:string" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">Appraiser Review Letter</value>
</values>
<values>
<field>Description__c</field>
<value xsi:type="xsd:string" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">Current appraiser letter template flow.</value>
</values>
<values>
<field>Active__c</field>
<value xsi:type="xsd:boolean" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">true</value>
</values>
<values>
<field>Is_Default__c</field>
<value xsi:type="xsd:boolean" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">true</value>
</values>
<values>
<field>Sort_Order__c</field>
<value xsi:type="xsd:double" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">10</value>
</values>
<values>
<field>Default_Destination_Document_Name_Prefix__c</field>
<value xsi:type="xsd:string" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">Review</value>
</values>
</CustomMetadata>

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<CustomMetadata xmlns="http://soap.sforce.com/2006/04/metadata">
<label>Education Letter</label>
<protected>false</protected>
<values><field>Account_Code__c</field><value xsi:type="xsd:string" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">DTC_IAM_Enterprise</value></values>
<values><field>Letter_Code__c</field><value xsi:type="xsd:string" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">EDUCATION_LETTER</value></values>
<values><field>Letter_Display_Name__c</field><value xsi:type="xsd:string" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">Education Letter</value></values>
<values><field>Description__c</field><value xsi:type="xsd:string" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">Education / guidance letter.</value></values>
<values><field>Active__c</field><value xsi:type="xsd:boolean" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">true</value></values>
<values><field>Is_Default__c</field><value xsi:type="xsd:boolean" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">false</value></values>
<values><field>Sort_Order__c</field><value xsi:type="xsd:double" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">40</value></values>
<values><field>Default_Destination_Document_Name_Prefix__c</field><value xsi:type="xsd:string" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">Education</value></values>
</CustomMetadata>

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<CustomMetadata xmlns="http://soap.sforce.com/2006/04/metadata">
<label>NOD Letter</label>
<protected>false</protected>
<values><field>Account_Code__c</field><value xsi:type="xsd:string" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">DTC_IAM_Enterprise</value></values>
<values><field>Letter_Code__c</field><value xsi:type="xsd:string" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">NOD_LETTER</value></values>
<values><field>Letter_Display_Name__c</field><value xsi:type="xsd:string" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">NOD Letter</value></values>
<values><field>Description__c</field><value xsi:type="xsd:string" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">Notice of Deficiency letter.</value></values>
<values><field>Active__c</field><value xsi:type="xsd:boolean" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">true</value></values>
<values><field>Is_Default__c</field><value xsi:type="xsd:boolean" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">false</value></values>
<values><field>Sort_Order__c</field><value xsi:type="xsd:double" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">20</value></values>
<values><field>Default_Destination_Document_Name_Prefix__c</field><value xsi:type="xsd:string" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">NOD</value></values>
</CustomMetadata>

View File

@ -0,0 +1,386 @@
<?xml version="1.0" encoding="UTF-8"?>
<FlexiPage xmlns="http://soap.sforce.com/2006/04/metadata">
<flexiPageRegions>
<itemInstances>
<componentInstance>
<componentInstanceProperties>
<name>collapsed</name>
<value>false</value>
</componentInstanceProperties>
<componentInstanceProperties>
<name>enableActionsConfiguration</name>
<value>false</value>
</componentInstanceProperties>
<componentInstanceProperties>
<name>enableActionsInNative</name>
<value>false</value>
</componentInstanceProperties>
<componentInstanceProperties>
<name>hideChatterActions</name>
<value>false</value>
</componentInstanceProperties>
<componentInstanceProperties>
<name>numVisibleActions</name>
<value>3</value>
</componentInstanceProperties>
<componentName>force:highlightsPanel</componentName>
<identifier>force_highlightsPanel</identifier>
</componentInstance>
</itemInstances>
<mode>Replace</mode>
<name>header</name>
<type>Region</type>
</flexiPageRegions>
<flexiPageRegions>
<itemInstances>
<componentInstance>
<componentInstanceProperties>
<name>relatedListComponentOverride</name>
<value>NONE</value>
</componentInstanceProperties>
<componentInstanceProperties>
<name>rowsToDisplay</name>
<value>10</value>
</componentInstanceProperties>
<componentInstanceProperties>
<name>showActionBar</name>
<value>true</value>
</componentInstanceProperties>
<componentName>force:relatedListContainer</componentName>
<identifier>force_relatedListContainer</identifier>
</componentInstance>
</itemInstances>
<mode>Replace</mode>
<name>relatedTabContent</name>
<type>Facet</type>
</flexiPageRegions>
<flexiPageRegions>
<itemInstances>
<fieldInstance>
<fieldInstanceProperties>
<name>uiBehavior</name>
<value>none</value>
</fieldInstanceProperties>
<fieldItem>Record.OwnerId</fieldItem>
<identifier>RecordOwnerIdField</identifier>
</fieldInstance>
</itemInstances>
<itemInstances>
<fieldInstance>
<fieldInstanceProperties>
<name>uiBehavior</name>
<value>none</value>
</fieldInstanceProperties>
<fieldItem>Record.Property_Street__c</fieldItem>
<identifier>RecordProperty_Street_cField</identifier>
</fieldInstance>
</itemInstances>
<itemInstances>
<fieldInstance>
<fieldInstanceProperties>
<name>uiBehavior</name>
<value>none</value>
</fieldInstanceProperties>
<fieldItem>Record.Property_City__c</fieldItem>
<identifier>RecordProperty_City_cField</identifier>
</fieldInstance>
</itemInstances>
<itemInstances>
<fieldInstance>
<fieldInstanceProperties>
<name>uiBehavior</name>
<value>none</value>
</fieldInstanceProperties>
<fieldItem>Record.Property_State_Province__c</fieldItem>
<identifier>RecordProperty_State_Province_cField</identifier>
</fieldInstance>
</itemInstances>
<itemInstances>
<fieldInstance>
<fieldInstanceProperties>
<name>uiBehavior</name>
<value>none</value>
</fieldInstanceProperties>
<fieldItem>Record.Property_Postal_Code__c</fieldItem>
<identifier>RecordProperty_Postal_Code_cField</identifier>
</fieldInstance>
</itemInstances>
<itemInstances>
<fieldInstance>
<fieldInstanceProperties>
<name>uiBehavior</name>
<value>none</value>
</fieldInstanceProperties>
<fieldItem>Record.Property_Country__c</fieldItem>
<identifier>RecordProperty_Country_cField</identifier>
</fieldInstance>
</itemInstances>
<name>Facet-f500e2fb-11c3-416b-9dc6-9d41da18f8b6</name>
<type>Facet</type>
</flexiPageRegions>
<flexiPageRegions>
<itemInstances>
<fieldInstance>
<fieldInstanceProperties>
<name>uiBehavior</name>
<value>none</value>
</fieldInstanceProperties>
<fieldItem>Record.Name</fieldItem>
<identifier>RecordNameField</identifier>
</fieldInstance>
</itemInstances>
<itemInstances>
<fieldInstance>
<fieldInstanceProperties>
<name>uiBehavior</name>
<value>none</value>
</fieldInstanceProperties>
<fieldItem>Record.Appraiser_Field_Review_Date__c</fieldItem>
<identifier>RecordAppraiser_Field_Review_Date_cField</identifier>
</fieldInstance>
</itemInstances>
<itemInstances>
<fieldInstance>
<fieldInstanceProperties>
<name>uiBehavior</name>
<value>none</value>
</fieldInstanceProperties>
<fieldItem>Record.CreatedById</fieldItem>
<identifier>RecordCreatedByIdField</identifier>
</fieldInstance>
</itemInstances>
<itemInstances>
<fieldInstance>
<fieldInstanceProperties>
<name>uiBehavior</name>
<value>none</value>
</fieldInstanceProperties>
<fieldItem>Record.LastModifiedById</fieldItem>
<identifier>RecordLastModifiedByIdField</identifier>
</fieldInstance>
</itemInstances>
<name>Facet-dc9d7e8f-5478-43ef-a95e-3f61960274fa</name>
<type>Facet</type>
</flexiPageRegions>
<flexiPageRegions>
<itemInstances>
<componentInstance>
<componentInstanceProperties>
<name>body</name>
<value>Facet-f500e2fb-11c3-416b-9dc6-9d41da18f8b6</value>
</componentInstanceProperties>
<componentName>flexipage:column</componentName>
<identifier>flexipage_column</identifier>
</componentInstance>
</itemInstances>
<itemInstances>
<componentInstance>
<componentInstanceProperties>
<name>body</name>
<value>Facet-dc9d7e8f-5478-43ef-a95e-3f61960274fa</value>
</componentInstanceProperties>
<componentName>flexipage:column</componentName>
<identifier>flexipage_column2</identifier>
</componentInstance>
</itemInstances>
<name>Facet-70cfd25b-1515-494b-91d2-98730c66f733</name>
<type>Facet</type>
</flexiPageRegions>
<flexiPageRegions>
<itemInstances>
<componentInstance>
<componentName>force:detailPanel</componentName>
<identifier>force_detailPanel</identifier>
</componentInstance>
</itemInstances>
<itemInstances>
<componentInstance>
<componentInstanceProperties>
<name>columns</name>
<value>Facet-70cfd25b-1515-494b-91d2-98730c66f733</value>
</componentInstanceProperties>
<componentInstanceProperties>
<name>horizontalAlignment</name>
<value>false</value>
</componentInstanceProperties>
<componentInstanceProperties>
<name>label</name>
<value>Section</value>
</componentInstanceProperties>
<componentName>flexipage:fieldSection</componentName>
<identifier>flexipage_fieldSection</identifier>
</componentInstance>
</itemInstances>
<mode>Replace</mode>
<name>detailTabContent</name>
<type>Facet</type>
</flexiPageRegions>
<flexiPageRegions>
<itemInstances>
<componentInstance>
<componentInstanceProperties>
<name>body</name>
<value>previewTabContent</value>
</componentInstanceProperties>
<componentInstanceProperties>
<name>title</name>
<value>CLM Preview</value>
</componentInstanceProperties>
<componentName>flexipage:tab</componentName>
<identifier>clmPreviewTab</identifier>
</componentInstance>
</itemInstances>
<itemInstances>
<componentInstance>
<componentInstanceProperties>
<name>body</name>
<value>esignTabContent</value>
</componentInstanceProperties>
<componentInstanceProperties>
<name>title</name>
<value>Docusign eSignature</value>
</componentInstanceProperties>
<componentName>flexipage:tab</componentName>
<identifier>esignWorkbenchTab</identifier>
</componentInstance>
</itemInstances>
<itemInstances>
<componentInstance>
<componentInstanceProperties>
<name>body</name>
<value>relatedTabContent</value>
</componentInstanceProperties>
<componentInstanceProperties>
<name>title</name>
<value>Standard.Tab.relatedLists</value>
</componentInstanceProperties>
<componentName>flexipage:tab</componentName>
<identifier>relatedListsTab</identifier>
</componentInstance>
</itemInstances>
<itemInstances>
<componentInstance>
<componentInstanceProperties>
<name>active</name>
<value>true</value>
</componentInstanceProperties>
<componentInstanceProperties>
<name>body</name>
<value>detailTabContent</value>
</componentInstanceProperties>
<componentInstanceProperties>
<name>title</name>
<value>Standard.Tab.detail</value>
</componentInstanceProperties>
<componentName>flexipage:tab</componentName>
<identifier>detailTab</identifier>
</componentInstance>
</itemInstances>
<mode>Replace</mode>
<name>maintabs</name>
<type>Facet</type>
</flexiPageRegions>
<flexiPageRegions>
<itemInstances>
<componentInstance>
<componentName>c:docusignEsignWorkbench</componentName>
<identifier>c_docusignEsignWorkbench</identifier>
</componentInstance>
</itemInstances>
<name>esignTabContent</name>
<type>Facet</type>
</flexiPageRegions>
<flexiPageRegions>
<itemInstances>
<componentInstance>
<componentName>c:clmRequestPreview</componentName>
<identifier>c_clmRequestPreview</identifier>
</componentInstance>
</itemInstances>
<name>previewTabContent</name>
<type>Facet</type>
</flexiPageRegions>
<flexiPageRegions>
<itemInstances>
<componentInstance>
<componentInstanceProperties>
<name>showLegacyActivityComposer</name>
<value>false</value>
</componentInstanceProperties>
<componentName>runtime_sales_activities:activityPanel</componentName>
<identifier>runtime_sales_activities_activityPanel</identifier>
</componentInstance>
</itemInstances>
<mode>Replace</mode>
<name>activityTabContent</name>
<type>Facet</type>
</flexiPageRegions>
<flexiPageRegions>
<itemInstances>
<componentInstance>
<componentInstanceProperties>
<name>active</name>
<value>true</value>
</componentInstanceProperties>
<componentInstanceProperties>
<name>body</name>
<value>activityTabContent</value>
</componentInstanceProperties>
<componentInstanceProperties>
<name>title</name>
<value>Standard.Tab.activity</value>
</componentInstanceProperties>
<componentName>flexipage:tab</componentName>
<identifier>activityTab</identifier>
</componentInstance>
</itemInstances>
<mode>Replace</mode>
<name>sidebartabs</name>
<type>Facet</type>
</flexiPageRegions>
<flexiPageRegions>
<itemInstances>
<componentInstance>
<componentInstanceProperties>
<name>label</name>
<value>Tabs</value>
</componentInstanceProperties>
<componentInstanceProperties>
<name>tabs</name>
<value>maintabs</value>
</componentInstanceProperties>
<componentName>flexipage:tabset</componentName>
<identifier>flexipage_tabset</identifier>
</componentInstance>
</itemInstances>
<mode>Replace</mode>
<name>main</name>
<type>Region</type>
</flexiPageRegions>
<flexiPageRegions>
<itemInstances>
<componentInstance>
<componentInstanceProperties>
<name>label</name>
<value>Tabs</value>
</componentInstanceProperties>
<componentInstanceProperties>
<name>tabs</name>
<value>sidebartabs</value>
</componentInstanceProperties>
<componentName>flexipage:tabset</componentName>
<identifier>flexipage_tabset2</identifier>
</componentInstance>
</itemInstances>
<mode>Replace</mode>
<name>sidebar</name>
<type>Region</type>
</flexiPageRegions>
<masterLabel>Appraiser Case Record Page</masterLabel>
<parentFlexiPage>flexipage__default_rec_L</parentFlexiPage>
<sobjectType>Appraiser_Case__c</sobjectType>
<template>
<name>flexipage:recordHomeTemplateDesktop</name>
</template>
<type>RecordPage</type>
</FlexiPage>

View File

@ -7,11 +7,11 @@
<label>Information</label> <label>Information</label>
<layoutColumns> <layoutColumns>
<layoutItems> <layoutItems>
<behavior>Required</behavior> <behavior>Readonly</behavior>
<field>Name</field> <field>Name</field>
</layoutItems> </layoutItems>
<layoutItems> <layoutItems>
<behavior>Required</behavior> <behavior>Edit</behavior>
<field>Appraiser_Case__c</field> <field>Appraiser_Case__c</field>
</layoutItems> </layoutItems>
<layoutItems> <layoutItems>
@ -19,12 +19,7 @@
<field>Deficiency_Number__c</field> <field>Deficiency_Number__c</field>
</layoutItems> </layoutItems>
</layoutColumns> </layoutColumns>
<layoutColumns> <layoutColumns/>
<layoutItems>
<behavior>Edit</behavior>
<field>Sort_Order__c</field>
</layoutItems>
</layoutColumns>
<style>TwoColumnsTopToBottom</style> <style>TwoColumnsTopToBottom</style>
</layoutSections> </layoutSections>
<layoutSections> <layoutSections>
@ -47,7 +42,7 @@
<style>TwoColumnsTopToBottom</style> <style>TwoColumnsTopToBottom</style>
</layoutSections> </layoutSections>
<showEmailCheckbox>false</showEmailCheckbox> <showEmailCheckbox>false</showEmailCheckbox>
<showHighlightsPanel>true</showHighlightsPanel> <showHighlightsPanel>false</showHighlightsPanel>
<showInteractionLogPanel>false</showInteractionLogPanel> <showInteractionLogPanel>false</showInteractionLogPanel>
<showRunAssignmentRulesCheckbox>false</showRunAssignmentRulesCheckbox> <showRunAssignmentRulesCheckbox>false</showRunAssignmentRulesCheckbox>
<showSubmitAndAttachButton>false</showSubmitAndAttachButton> <showSubmitAndAttachButton>false</showSubmitAndAttachButton>

View File

@ -7,22 +7,79 @@
<label>Information</label> <label>Information</label>
<layoutColumns> <layoutColumns>
<layoutItems> <layoutItems>
<behavior>Required</behavior> <behavior>Readonly</behavior>
<field>Name</field> <field>Name</field>
</layoutItems> </layoutItems>
<layoutItems> <layoutItems>
<behavior>Edit</behavior> <behavior>Edit</behavior>
<field>Appraiser_Field_Review_Date__c</field> <field>Appraiser_Field_Review_Date__c</field>
</layoutItems> </layoutItems>
</layoutColumns>
<layoutColumns/>
<style>TwoColumnsTopToBottom</style>
</layoutSections>
<layoutSections>
<customLabel>false</customLabel>
<detailHeading>true</detailHeading>
<editHeading>true</editHeading>
<label>Letter Header</label>
<layoutColumns>
<layoutItems> <layoutItems>
<behavior>Edit</behavior> <behavior>Edit</behavior>
<field>CreatedDate</field> <field>Appraiser_Name__c</field>
</layoutItems>
<layoutItems>
<behavior>Edit</behavior>
<field>Appraiser_Salutation__c</field>
</layoutItems>
<layoutItems>
<behavior>Edit</behavior>
<field>Appraiser_Last_Name__c</field>
</layoutItems>
<layoutItems>
<behavior>Edit</behavior>
<field>Letter_Sent_Date__c</field>
</layoutItems> </layoutItems>
</layoutColumns> </layoutColumns>
<layoutColumns> <layoutColumns>
<layoutItems> <layoutItems>
<behavior>Edit</behavior> <behavior>Edit</behavior>
<field>LastModifiedDate</field> <field>FHA_Case_Number__c</field>
</layoutItems>
<layoutItems>
<behavior>Edit</behavior>
<field>Appraiser_Email__c</field>
</layoutItems>
</layoutColumns>
<style>TwoColumnsTopToBottom</style>
</layoutSections>
<layoutSections>
<customLabel>false</customLabel>
<detailHeading>true</detailHeading>
<editHeading>true</editHeading>
<label>Appraiser Address</label>
<layoutColumns>
<layoutItems>
<behavior>Edit</behavior>
<field>Appraiser_Street__c</field>
</layoutItems>
<layoutItems>
<behavior>Edit</behavior>
<field>Appraiser_City__c</field>
</layoutItems>
<layoutItems>
<behavior>Edit</behavior>
<field>Appraiser_State_Province__c</field>
</layoutItems>
</layoutColumns>
<layoutColumns>
<layoutItems>
<behavior>Edit</behavior>
<field>Appraiser_Postal_Code__c</field>
</layoutItems>
<layoutItems>
<behavior>Edit</behavior>
<field>Appraiser_Country__c</field>
</layoutItems> </layoutItems>
</layoutColumns> </layoutColumns>
<style>TwoColumnsTopToBottom</style> <style>TwoColumnsTopToBottom</style>
@ -58,19 +115,94 @@
</layoutColumns> </layoutColumns>
<style>TwoColumnsTopToBottom</style> <style>TwoColumnsTopToBottom</style>
</layoutSections> </layoutSections>
<layoutSections>
<customLabel>false</customLabel>
<detailHeading>true</detailHeading>
<editHeading>true</editHeading>
<label>Doc Gen Tracking</label>
<layoutColumns>
<layoutItems>
<behavior>Readonly</behavior>
<field>Last_DocGen_Status__c</field>
</layoutItems>
<layoutItems>
<behavior>Readonly</behavior>
<field>Last_CLM_Account_Code__c</field>
</layoutItems>
<layoutItems>
<behavior>Readonly</behavior>
<field>Last_DocGen_Task_Id__c</field>
</layoutItems>
<layoutItems>
<behavior>Readonly</behavior>
<field>Generated_Document_Id__c</field>
</layoutItems>
<layoutItems>
<behavior>Readonly</behavior>
<field>Attached_File_Content_Document_Id__c</field>
</layoutItems>
<layoutItems>
<behavior>Readonly</behavior>
<field>Last_DocGen_Requested_At__c</field>
</layoutItems>
<layoutItems>
<behavior>Readonly</behavior>
<field>Last_DocGen_Completed_At__c</field>
</layoutItems>
</layoutColumns>
<layoutColumns>
<layoutItems>
<behavior>Readonly</behavior>
<field>Last_DocGen_Task_Url__c</field>
</layoutItems>
<layoutItems>
<behavior>Readonly</behavior>
<field>Generated_Document_Url__c</field>
</layoutItems>
<layoutItems>
<behavior>Readonly</behavior>
<field>Attached_File_Url__c</field>
</layoutItems>
<layoutItems>
<behavior>Readonly</behavior>
<field>Last_Template_Document_Href__c</field>
</layoutItems>
<layoutItems>
<behavior>Readonly</behavior>
<field>Last_Destination_Folder_Href__c</field>
</layoutItems>
<layoutItems>
<behavior>Readonly</behavior>
<field>Last_DocGen_Message__c</field>
</layoutItems>
</layoutColumns>
<style>TwoColumnsTopToBottom</style>
</layoutSections>
<platformActionList>
<actionListContext>Record</actionListContext>
<platformActionListItems>
<actionName>Appraiser_Case__c.Generate_Review_Letter</actionName>
<actionType>QuickAction</actionType>
<sortOrder>0</sortOrder>
</platformActionListItems>
<platformActionListItems>
<actionName>Edit</actionName>
<actionType>StandardButton</actionType>
<sortOrder>1</sortOrder>
</platformActionListItems>
<platformActionListItems>
<actionName>Delete</actionName>
<actionType>StandardButton</actionType>
<sortOrder>2</sortOrder>
</platformActionListItems>
</platformActionList>
<relatedLists> <relatedLists>
<fields>NAME</fields> <fields>NAME</fields>
<fields>Deficiency_Number__c</fields> <fields>Deficiency_Number__c</fields>
<fields>Description__c</fields> <fields>Description__c</fields>
<fields>Reference__c</fields>
<fields>Resolution__c</fields> <fields>Resolution__c</fields>
<fields>Sort_Order__c</fields> <relatedList>Appraiser_Case_Deficiency__c.Appraiser_Case__c</relatedList>
<relatedList>Appraiser_Deficiencies__r</relatedList>
</relatedLists>
<relatedLists>
<fields>SUBJECT</fields>
<fields>STATUS</fields>
<fields>DUE_DATE</fields>
<relatedList>OpenActivities</relatedList>
</relatedLists> </relatedLists>
<showEmailCheckbox>false</showEmailCheckbox> <showEmailCheckbox>false</showEmailCheckbox>
<showHighlightsPanel>true</showHighlightsPanel> <showHighlightsPanel>true</showHighlightsPanel>

View File

@ -0,0 +1,112 @@
<?xml version="1.0" encoding="UTF-8"?>
<Layout xmlns="http://soap.sforce.com/2006/04/metadata">
<layoutSections>
<customLabel>false</customLabel>
<detailHeading>true</detailHeading>
<editHeading>true</editHeading>
<label>Account Details</label>
<layoutColumns>
<layoutItems>
<behavior>Required</behavior>
<field>MasterLabel</field>
</layoutItems>
<layoutItems>
<behavior>Required</behavior>
<field>DeveloperName</field>
</layoutItems>
<layoutItems>
<behavior>Edit</behavior>
<field>Account_Code__c</field>
</layoutItems>
<layoutItems>
<behavior>Edit</behavior>
<field>Account_Display_Name__c</field>
</layoutItems>
<layoutItems>
<behavior>Edit</behavior>
<field>Active__c</field>
</layoutItems>
</layoutColumns>
<layoutColumns>
<layoutItems>
<behavior>Edit</behavior>
<field>Environment_Code__c</field>
</layoutItems>
<layoutItems>
<behavior>Edit</behavior>
<field>NamespacePrefix</field>
</layoutItems>
<layoutItems>
<behavior>Edit</behavior>
<field>IsProtected</field>
</layoutItems>
<layoutItems>
<behavior>Edit</behavior>
<field>CLM_Account_Id__c</field>
</layoutItems>
<layoutItems>
<behavior>Edit</behavior>
<field>Default_Destination_Document_Name_Prefix__c</field>
</layoutItems>
</layoutColumns>
<style>TwoColumnsTopToBottom</style>
</layoutSections>
<layoutSections>
<customLabel>false</customLabel>
<detailHeading>true</detailHeading>
<editHeading>true</editHeading>
<label>CLM API Configuration</label>
<layoutColumns>
<layoutItems>
<behavior>Edit</behavior>
<field>CLM_Api_Named_Credential__c</field>
</layoutItems>
<layoutItems>
<behavior>Edit</behavior>
<field>CLM_Download_Named_Credential__c</field>
</layoutItems>
</layoutColumns>
<layoutColumns>
<layoutItems>
<behavior>Edit</behavior>
<field>Template_Root_Folder_Href__c</field>
</layoutItems>
<layoutItems>
<behavior>Edit</behavior>
<field>Destination_Root_Folder_Href__c</field>
</layoutItems>
<layoutItems>
<behavior>Edit</behavior>
<field>Default_Template_Document_Href__c</field>
</layoutItems>
</layoutColumns>
<style>TwoColumnsTopToBottom</style>
</layoutSections>
<layoutSections>
<customLabel>false</customLabel>
<detailHeading>true</detailHeading>
<editHeading>true</editHeading>
<label>eSignature API Configuration</label>
<layoutColumns>
<layoutItems>
<behavior>Edit</behavior>
<field>ESignature_Auth_Named_Credential__c</field>
</layoutItems>
<layoutItems>
<behavior>Edit</behavior>
<field>ESignature_Rest_Named_Credential__c</field>
</layoutItems>
<layoutItems>
<behavior>Edit</behavior>
<field>ESignature_Account_Id__c</field>
</layoutItems>
</layoutColumns>
<layoutColumns/>
<style>TwoColumnsTopToBottom</style>
</layoutSections>
<showEmailCheckbox>false</showEmailCheckbox>
<showHighlightsPanel>false</showHighlightsPanel>
<showInteractionLogPanel>false</showInteractionLogPanel>
<showRunAssignmentRulesCheckbox>false</showRunAssignmentRulesCheckbox>
<showSubmitAndAttachButton>false</showSubmitAndAttachButton>
</Layout>

View File

@ -0,0 +1,90 @@
<?xml version="1.0" encoding="UTF-8"?>
<Layout xmlns="http://soap.sforce.com/2006/04/metadata">
<layoutSections>
<customLabel>false</customLabel>
<detailHeading>true</detailHeading>
<editHeading>true</editHeading>
<label>Letter Definition</label>
<layoutColumns>
<layoutItems>
<behavior>Required</behavior>
<field>MasterLabel</field>
</layoutItems>
<layoutItems>
<behavior>Required</behavior>
<field>DeveloperName</field>
</layoutItems>
<layoutItems>
<behavior>Edit</behavior>
<field>Account_Code__c</field>
</layoutItems>
<layoutItems>
<behavior>Edit</behavior>
<field>Letter_Code__c</field>
</layoutItems>
<layoutItems>
<behavior>Edit</behavior>
<field>Letter_Display_Name__c</field>
</layoutItems>
<layoutItems>
<behavior>Edit</behavior>
<field>Description__c</field>
</layoutItems>
</layoutColumns>
<layoutColumns>
<layoutItems>
<behavior>Edit</behavior>
<field>Active__c</field>
</layoutItems>
<layoutItems>
<behavior>Edit</behavior>
<field>Is_Default__c</field>
</layoutItems>
<layoutItems>
<behavior>Edit</behavior>
<field>Sort_Order__c</field>
</layoutItems>
<layoutItems>
<behavior>Edit</behavior>
<field>NamespacePrefix</field>
</layoutItems>
<layoutItems>
<behavior>Edit</behavior>
<field>IsProtected</field>
</layoutItems>
</layoutColumns>
<style>TwoColumnsTopToBottom</style>
</layoutSections>
<layoutSections>
<customLabel>false</customLabel>
<detailHeading>true</detailHeading>
<editHeading>true</editHeading>
<label>CLM Defaults</label>
<layoutColumns>
<layoutItems>
<behavior>Edit</behavior>
<field>Template_Root_Folder_Href__c</field>
</layoutItems>
<layoutItems>
<behavior>Edit</behavior>
<field>Destination_Root_Folder_Href__c</field>
</layoutItems>
</layoutColumns>
<layoutColumns>
<layoutItems>
<behavior>Edit</behavior>
<field>Default_Template_Document_Href__c</field>
</layoutItems>
<layoutItems>
<behavior>Edit</behavior>
<field>Default_Destination_Document_Name_Prefix__c</field>
</layoutItems>
</layoutColumns>
<style>TwoColumnsTopToBottom</style>
</layoutSections>
<showEmailCheckbox>false</showEmailCheckbox>
<showHighlightsPanel>false</showHighlightsPanel>
<showInteractionLogPanel>false</showInteractionLogPanel>
<showRunAssignmentRulesCheckbox>false</showRunAssignmentRulesCheckbox>
<showSubmitAndAttachButton>false</showSubmitAndAttachButton>
</Layout>

View File

@ -0,0 +1,80 @@
.panel {
display: grid;
gap: 1rem;
padding: 1rem;
}
.section {
border: 1px solid #d8dde6;
border-radius: 0.5rem;
padding: 1rem;
display: grid;
gap: 0.75rem;
}
.section-title {
font-size: 0.95rem;
font-weight: 700;
margin: 0;
}
.button-row {
display: flex;
gap: 0.75rem;
flex-wrap: wrap;
}
.hint {
color: #3e3e3c;
font-size: 0.85rem;
margin: 0;
}
.result {
border-radius: 0.5rem;
padding: 0.75rem 1rem;
}
.result-success {
background: #e8f5e9;
color: #1b5e20;
}
.result-error {
background: #fdecea;
color: #8a1f11;
}
.result-info {
background: #eef4ff;
color: #16325c;
}
.deficiency-list {
display: grid;
gap: 0.75rem;
}
.deficiency-item {
border-top: 1px solid #d8dde6;
padding-top: 0.75rem;
}
.json-block {
margin-top: 0.75rem;
border-top: 1px solid rgba(0, 0, 0, 0.1);
padding-top: 0.75rem;
}
.json-title {
font-weight: 700;
margin: 0 0 0.5rem;
}
.json-block pre {
margin: 0;
white-space: pre-wrap;
word-break: break-word;
font-size: 0.8rem;
line-height: 1.4;
}

View File

@ -0,0 +1,200 @@
<template>
<lightning-card title="CLM Doc Gen Workbench" icon-name="standard:document_reference">
<div class="panel">
<lightning-combobox
label="CLM Account"
value={accountCode}
options={accountOptions}
onchange={handleAccountChange}
></lightning-combobox>
<lightning-combobox
label="Letter Type"
value={letterCode}
options={letterOptions}
onchange={handleLetterChange}
></lightning-combobox>
<template if:true={selectedAccountEnvironment}>
<p class="hint">Environment: {selectedAccountEnvironment}</p>
</template>
<template if:true={selectedLetterDescription}>
<p class="hint">{selectedLetterDescription}</p>
</template>
<div class="button-row">
<lightning-button
label="Reset To Defaults"
onclick={resetSelectionsToDefaults}
disabled={isBusy}
></lightning-button>
</div>
<template if:true={caseContext}>
<div class="section">
<h3 class="section-title">Case Summary</h3>
<p class="hint">Case: {caseContext.caseNumber}</p>
<template if:true={caseContext.propertyAddress}>
<p class="hint">Property: {caseContext.propertyAddress}</p>
</template>
<template if:true={hasDeficiencies}>
<div class="deficiency-list">
<template for:each={caseContext.deficiencies} for:item="deficiency">
<div key={deficiency.recordId} class="deficiency-item">
<p><strong>#{deficiency.deficiencyNumber}</strong> {deficiency.description}</p>
<p class="hint">{deficiency.resolution}</p>
</div>
</template>
</div>
</template>
</div>
</template>
<div class="section">
<h3 class="section-title">Template Browser</h3>
<lightning-input
label="Template Folder Href"
value={templateFolderHref}
onchange={handleTemplateFolderHrefChange}
></lightning-input>
<div class="button-row">
<lightning-button
label="Load Templates"
onclick={loadTemplateFolder}
disabled={isBusy}
></lightning-button>
<lightning-button
label="Open Selected Template Folder"
onclick={openSelectedTemplateFolder}
disabled={disableTemplateFolderOpen}
></lightning-button>
<lightning-button
label="Up One Level"
onclick={openTemplateParentFolder}
disabled={disableTemplateFolderUp}
></lightning-button>
</div>
<template if:true={templateFolderName}>
<p class="hint">Current folder: {templateFolderName}</p>
</template>
<lightning-combobox
label="Template Subfolders"
value={selectedTemplateSubfolderHref}
options={templateSubfolderOptions}
onchange={handleTemplateSubfolderChange}
></lightning-combobox>
<lightning-combobox
label="Template Documents"
value={templateDocHref}
options={templateDocumentOptions}
onchange={handleTemplateDocHrefChange}
></lightning-combobox>
<lightning-input
label="Template Document Href"
value={templateDocHref}
onchange={handleTemplateDocHrefChange}
></lightning-input>
<template if:true={templateDocHref}>
<p class="hint">Selected template: {selectedTemplateSummary}</p>
</template>
</div>
<div class="section">
<h3 class="section-title">Destination Browser</h3>
<lightning-input
label="Destination Folder Href"
value={destinationFolderHref}
onchange={handleDestinationFolderHrefChange}
></lightning-input>
<div class="button-row">
<lightning-button
label="Load Destination Folder"
onclick={loadDestinationFolder}
disabled={isBusy}
></lightning-button>
<lightning-button
label="Open Selected Destination Folder"
onclick={openSelectedDestinationFolder}
disabled={disableDestinationFolderOpen}
></lightning-button>
<lightning-button
label="Up One Level"
onclick={openDestinationParentFolder}
disabled={disableDestinationFolderUp}
></lightning-button>
</div>
<template if:true={destinationFolderName}>
<p class="hint">Current folder: {destinationFolderName}</p>
</template>
<lightning-combobox
label="Destination Folder Documents"
value={destinationDocName}
options={destinationDocumentOptions}
onchange={handleDestinationDocumentSelection}
></lightning-combobox>
<lightning-combobox
label="Destination Subfolders"
value={selectedDestinationSubfolderHref}
options={destinationSubfolderOptions}
onchange={handleDestinationSubfolderChange}
></lightning-combobox>
<lightning-input
label="Destination Filename"
value={destinationDocName}
onchange={handleDestinationNameChange}
></lightning-input>
<lightning-input
label="Selected Destination Folder Href"
value={destinationFolderHref}
onchange={handleDestinationFolderHrefChange}
></lightning-input>
</div>
<div class="button-row">
<lightning-button
variant="brand"
label="Generate Document"
onclick={generateDocument}
disabled={disableGenerate}
></lightning-button>
<lightning-button
label="Check Task Status"
onclick={checkTaskStatus}
disabled={disableStatusCheck}
></lightning-button>
<lightning-button
label="Attach Generated Document"
onclick={attachGeneratedDocument}
disabled={disableAttachGeneratedDocument}
></lightning-button>
<template if:true={showCloseButton}>
<lightning-button
label="Close"
onclick={closeAction}
></lightning-button>
</template>
</div>
<template if:true={resultMessage}>
<div class={resultClass}>
<p>{resultMessage}</p>
<template if:true={taskStatus}>
<p>Status: {taskStatus}</p>
</template>
<template if:true={taskId}>
<p>Task ID: {taskId}</p>
</template>
<template if:true={generatedDocumentId}>
<p>Generated Document ID: {generatedDocumentId}</p>
</template>
<template if:true={hasAttachedSalesforceFile}>
<p><lightning-formatted-url value={attachedSalesforceFileUrl} label="Open attached Salesforce file"></lightning-formatted-url></p>
</template>
<template if:true={hasTaskDetails}>
<div class="json-block">
<p class="json-title">Task Details</p>
<pre>{taskDetailsJson}</pre>
</div>
</template>
</div>
</template>
</div>
</lightning-card>
</template>

View File

@ -0,0 +1,503 @@
import { LightningElement, api, wire } from 'lwc';
import { getRecord } from 'lightning/uiRecordApi';
import { ShowToastEvent } from 'lightning/platformShowToastEvent';
import { CloseActionScreenEvent } from 'lightning/actions';
import getCaseContext from '@salesforce/apex/CLMAdminService.getCaseContext';
import attachGeneratedDocumentToCase from '@salesforce/apex/CLMAdminService.attachGeneratedDocumentToCase';
import generateDocument from '@salesforce/apex/CLMAdminService.generateDocument';
import getAccountSettings from '@salesforce/apex/CLMAdminService.getAccountSettings';
import getLetterSettings from '@salesforce/apex/CLMAdminService.getLetterSettings';
import listAccountSettings from '@salesforce/apex/CLMAdminService.listAccountSettings';
import listLetterSettings from '@salesforce/apex/CLMAdminService.listLetterSettings';
import getFolderContents from '@salesforce/apex/CLMAdminService.getFolderContents';
import getTaskStatus from '@salesforce/apex/CLMAdminService.getTaskStatus';
const CASE_FIELDS = ['Appraiser_Case__c.Name'];
export default class ClmDocGenWorkbench extends LightningElement {
@api recordId;
@api objectApiName;
accountCode = '';
destinationDocName = '';
templateFolderHref = '';
templateDocHref = '';
destinationFolderHref = '';
templateFolderName = '';
destinationFolderName = '';
templateParentFolderHref = '';
destinationParentFolderHref = '';
templateSubfolderOptions = [];
templateDocumentOptions = [];
destinationSubfolderOptions = [];
destinationDocumentOptions = [];
selectedTemplateSubfolderHref = '';
selectedDestinationSubfolderHref = '';
resultMessage = '';
resultVariant = 'info';
taskId = '';
taskStatus = '';
taskDetailsJson = '';
attachedFileUrl = '';
attachedFileTitle = '';
isBusy = false;
hasLoadedDefaults = false;
caseNumber = '';
caseContext;
accountOptions = [];
selectedAccountSettings;
letterCode = '';
letterOptions = [];
selectedLetterSettings;
@wire(getRecord, { recordId: '$recordId', fields: CASE_FIELDS })
wiredCase({ data }) {
if (data) {
this.caseNumber = data.fields.Name.value;
}
if (data && !this.destinationDocName) {
this.destinationDocName = this.buildDefaultDocumentName('Review');
}
if (data && !this.hasLoadedDefaults) {
this.initializeDefaults();
}
if (data && !this.caseContext) {
this.loadCaseContext();
}
}
get disableGenerate() {
return this.isBusy || !this.recordId || !this.accountCode || !this.templateDocHref || !this.destinationFolderHref || !this.destinationDocName;
}
get disableStatusCheck() {
return this.isBusy || !this.taskId;
}
get disableTemplateFolderOpen() {
return this.isBusy || !this.selectedTemplateSubfolderHref;
}
get disableTemplateFolderUp() {
return this.isBusy || !this.templateParentFolderHref;
}
get disableDestinationFolderOpen() {
return this.isBusy || !this.selectedDestinationSubfolderHref;
}
get disableDestinationFolderUp() {
return this.isBusy || !this.destinationParentFolderHref;
}
get resultClass() {
return `result result-${this.resultVariant}`;
}
get hasDeficiencies() {
return this.caseContext && this.caseContext.deficiencies && this.caseContext.deficiencies.length > 0;
}
get selectedTemplateSummary() {
const selected = this.templateDocumentOptions.find((item) => item.value === this.templateDocHref);
return selected ? selected.label : this.templateDocHref;
}
get lastTaskUrl() {
return this.caseContext ? this.caseContext.lastDocGenTaskUrl : null;
}
get generatedDocumentUrl() {
return this.caseContext ? this.caseContext.generatedDocumentUrl : null;
}
get generatedDocumentId() {
return this.caseContext ? this.caseContext.generatedDocumentId : null;
}
get attachedSalesforceFileUrl() {
return this.attachedFileUrl || (this.caseContext ? this.caseContext.attachedFileUrl : null);
}
get hasAttachedSalesforceFile() {
return Boolean(this.attachedSalesforceFileUrl);
}
get disableAttachGeneratedDocument() {
return this.isBusy || !this.generatedDocumentId;
}
get hasTaskDetails() {
return Boolean(this.taskDetailsJson);
}
get showCloseButton() {
return true;
}
get selectedAccountEnvironment() {
return this.selectedAccountSettings ? this.selectedAccountSettings.environment : '';
}
get selectedLetterDescription() {
return this.selectedLetterSettings ? this.selectedLetterSettings.description : '';
}
async handleAccountChange(event) {
this.accountCode = event.detail.value;
await this.initializeDefaults(true);
}
async handleLetterChange(event) {
this.letterCode = event.detail.value;
await this.initializeDefaults(true);
}
handleDestinationNameChange(event) {
this.destinationDocName = event.target.value;
}
handleTemplateFolderHrefChange(event) {
this.templateFolderHref = event.target.value;
}
handleTemplateSubfolderChange(event) {
this.selectedTemplateSubfolderHref = event.detail.value;
}
handleTemplateDocHrefChange(event) {
this.templateDocHref = event.detail.value || event.target.value;
}
handleDestinationFolderHrefChange(event) {
this.destinationFolderHref = event.target.value;
}
handleDestinationSubfolderChange(event) {
this.selectedDestinationSubfolderHref = event.detail.value;
}
handleDestinationDocumentSelection(event) {
this.destinationDocName = event.detail.value;
}
async loadTemplateFolder() {
await this.loadFolder('template', this.templateFolderHref);
}
async openSelectedTemplateFolder() {
this.templateFolderHref = this.selectedTemplateSubfolderHref;
await this.loadTemplateFolder();
}
async openTemplateParentFolder() {
this.templateFolderHref = this.templateParentFolderHref;
await this.loadTemplateFolder();
}
async loadDestinationFolder() {
await this.loadFolder('destination', this.destinationFolderHref);
}
async openSelectedDestinationFolder() {
this.destinationFolderHref = this.selectedDestinationSubfolderHref;
await this.loadDestinationFolder();
}
async openDestinationParentFolder() {
this.destinationFolderHref = this.destinationParentFolderHref;
await this.loadDestinationFolder();
}
async initializeDefaults(forceReload = false) {
if (this.hasLoadedDefaults && !forceReload) {
return;
}
this.isBusy = true;
this.clearResult();
try {
await this.loadAccountOptions();
const settings = await getAccountSettings({ accountCode: this.accountCode });
await this.loadLetterOptions(forceReload);
const letterSettings = await getLetterSettings({ accountCode: this.accountCode, letterCode: this.letterCode });
await this.applySettings(settings, letterSettings, forceReload);
this.hasLoadedDefaults = true;
} catch (error) {
this.handleError(error, 'Unable to load CLM account defaults');
} finally {
this.isBusy = false;
}
}
async applySettings(settings, letterSettings, forceReload) {
this.selectedAccountSettings = settings;
this.selectedLetterSettings = letterSettings;
const prefix = letterSettings && letterSettings.defaultDocumentNamePrefix
? letterSettings.defaultDocumentNamePrefix
: settings && settings.defaultDocumentNamePrefix
? settings.defaultDocumentNamePrefix
: 'Review';
if (forceReload || !this.destinationDocName) {
this.destinationDocName = this.buildDefaultDocumentName(prefix);
}
if (settings || letterSettings) {
if (letterSettings && letterSettings.templateRootFolderHref) {
this.templateFolderHref = letterSettings.templateRootFolderHref;
} else if (settings && settings.templateRootFolderHref) {
this.templateFolderHref = settings.templateRootFolderHref;
}
if ((forceReload || !this.templateDocHref) && letterSettings && letterSettings.defaultTemplateDocumentHref) {
this.templateDocHref = letterSettings.defaultTemplateDocumentHref;
} else if ((forceReload || !this.templateDocHref) && settings && settings.defaultTemplateDocumentHref) {
this.templateDocHref = settings.defaultTemplateDocumentHref;
}
if (letterSettings && letterSettings.destinationRootFolderHref) {
this.destinationFolderHref = letterSettings.destinationRootFolderHref;
} else if (settings && settings.destinationRootFolderHref) {
this.destinationFolderHref = settings.destinationRootFolderHref;
}
}
if (this.templateFolderHref) {
await this.loadFolder('template', this.templateFolderHref, true);
}
if (this.destinationFolderHref) {
await this.loadFolder('destination', this.destinationFolderHref, true);
}
}
async loadAccountOptions() {
const accounts = await listAccountSettings();
this.accountOptions = (accounts || []).map((account) => ({
label: account.accountDisplayName,
value: account.accountCode
}));
if (!this.accountCode) {
if (this.caseContext && this.caseContext.lastClmAccountCode) {
this.accountCode = this.caseContext.lastClmAccountCode;
} else if (this.accountOptions.length > 0) {
this.accountCode = this.accountOptions[0].value;
}
}
}
async loadLetterOptions(forceReload) {
const letters = await listLetterSettings({ accountCode: this.accountCode });
this.letterOptions = (letters || []).map((letter) => ({
label: letter.letterDisplayName,
value: letter.letterCode
}));
const hasExistingSelection = this.letterOptions.some((letter) => letter.value === this.letterCode);
if (!hasExistingSelection || forceReload || !this.letterCode) {
const defaultLetter = (letters || []).find((letter) => letter.isDefault);
this.letterCode = defaultLetter
? defaultLetter.letterCode
: this.letterOptions.length > 0
? this.letterOptions[0].value
: '';
}
}
buildDefaultDocumentName(prefix) {
const normalizedPrefix = prefix || 'Review';
if (this.caseNumber) {
return `${normalizedPrefix}_${this.caseNumber}.docx`;
}
return `${normalizedPrefix}.docx`;
}
async resetSelectionsToDefaults() {
this.hasLoadedDefaults = false;
this.templateFolderName = '';
this.destinationFolderName = '';
this.templateParentFolderHref = '';
this.destinationParentFolderHref = '';
this.templateSubfolderOptions = [];
this.templateDocumentOptions = [];
this.destinationSubfolderOptions = [];
this.destinationDocumentOptions = [];
this.selectedTemplateSubfolderHref = '';
this.selectedDestinationSubfolderHref = '';
await this.initializeDefaults(true);
}
async loadCaseContext() {
if (!this.recordId) {
return;
}
try {
this.caseContext = await getCaseContext({ appraiserCaseId: this.recordId });
if (this.caseContext && this.caseContext.caseNumber) {
this.caseNumber = this.caseContext.caseNumber;
}
if (this.caseContext && this.caseContext.lastDocGenTaskId) {
this.taskId = this.caseContext.lastDocGenTaskId;
}
if (this.caseContext && this.caseContext.lastClmAccountCode && this.accountCode !== this.caseContext.lastClmAccountCode) {
this.accountCode = this.caseContext.lastClmAccountCode;
if (this.hasLoadedDefaults) {
await this.initializeDefaults(true);
}
}
if (this.caseContext && this.caseContext.lastDocGenStatus && !this.taskStatus) {
this.taskStatus = this.caseContext.lastDocGenStatus;
}
if (this.caseContext && this.caseContext.attachedFileUrl && !this.attachedFileUrl) {
this.attachedFileUrl = this.caseContext.attachedFileUrl;
}
} catch (error) {
this.handleError(error, 'Unable to load case context');
}
}
async loadFolder(kind, folderHref, preserveResult = false) {
if (!folderHref) {
this.showToast('Missing folder href', 'Enter a folder href before loading.', 'warning');
return;
}
this.isBusy = true;
if (!preserveResult) {
this.clearResult();
}
try {
const contents = await getFolderContents({ folderHref, accountCode: this.accountCode });
const subfolders = (contents.folders || []).map((item) => ({ label: item.name, value: item.href }));
if (kind === 'template') {
this.templateFolderHref = contents.folder ? contents.folder.href : folderHref;
this.templateFolderName = contents.folder ? contents.folder.name : '';
this.templateParentFolderHref = contents.folder ? contents.folder.parentHref : '';
this.templateSubfolderOptions = subfolders;
this.templateDocumentOptions = (contents.documents || []).map((item) => ({ label: item.name, value: item.href }));
this.selectedTemplateSubfolderHref = '';
} else {
this.destinationFolderHref = contents.folder ? contents.folder.href : folderHref;
this.destinationFolderName = contents.folder ? contents.folder.name : '';
this.destinationParentFolderHref = contents.folder ? contents.folder.parentHref : '';
this.destinationSubfolderOptions = subfolders;
this.destinationDocumentOptions = (contents.documents || []).map((item) => ({ label: item.name, value: item.name }));
this.selectedDestinationSubfolderHref = '';
}
} catch (error) {
this.handleError(error, 'Unable to load folder contents');
} finally {
this.isBusy = false;
}
}
async generateDocument() {
this.isBusy = true;
this.clearResult();
try {
const response = await generateDocument({
appraiserCaseId: this.recordId,
templateDocHref: this.templateDocHref,
destinationFolderHref: this.destinationFolderHref,
destinationDocName: this.destinationDocName,
accountCode: this.accountCode
});
this.taskId = response.documentId;
this.taskStatus = response.taskStatus || '';
this.taskDetailsJson = this.formatJsonString(response.taskDetailsJson);
this.resultVariant = response.success ? 'success' : 'error';
this.resultMessage = response.message;
await this.loadCaseContext();
this.showToast(response.success ? 'Document submitted' : 'Submission failed', response.message, response.success ? 'success' : 'error');
} catch (error) {
this.handleError(error, 'Document generation failed');
} finally {
this.isBusy = false;
}
}
async checkTaskStatus() {
this.isBusy = true;
this.clearResult();
try {
const response = await getTaskStatus({ appraiserCaseId: this.recordId, taskId: this.taskId, accountCode: this.accountCode });
this.taskStatus = response.taskStatus || '';
this.taskDetailsJson = this.formatJsonString(response.taskDetailsJson);
this.resultVariant = response.success ? 'success' : 'error';
this.resultMessage = response.message;
await this.loadCaseContext();
this.showToast('Task status updated', response.message, response.success ? 'success' : 'error');
} catch (error) {
this.handleError(error, 'Unable to fetch task status');
} finally {
this.isBusy = false;
}
}
async attachGeneratedDocument() {
this.isBusy = true;
try {
const result = await attachGeneratedDocumentToCase({
appraiserCaseId: this.recordId,
accountCode: this.accountCode
});
this.resultVariant = result.success ? 'success' : 'error';
this.resultMessage = result.message;
this.attachedFileUrl = result.fileUrl || '';
this.attachedFileTitle = result.fileTitle || '';
await this.loadCaseContext();
this.showToast('Document attached', result.message, result.success ? 'success' : 'error');
} catch (error) {
this.handleError(error, 'Unable to attach generated document');
} finally {
this.isBusy = false;
}
}
closeAction() {
this.dispatchEvent(new CloseActionScreenEvent());
}
clearResult() {
this.resultMessage = '';
this.resultVariant = 'info';
this.taskDetailsJson = '';
}
handleError(error, title) {
const bodyMessage = error && error.body ? error.body.message : null;
const directMessage = error ? error.message : null;
const message = bodyMessage || directMessage || 'Unknown error';
this.resultVariant = 'error';
this.resultMessage = message;
this.showToast(title, message, 'error');
}
showToast(title, message, variant) {
this.dispatchEvent(new ShowToastEvent({ title, message, variant }));
}
formatJsonString(value) {
if (!value) {
return '';
}
try {
return JSON.stringify(JSON.parse(value), null, 2);
} catch (e) {
return value;
}
}
}

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>63.0</apiVersion>
<isExposed>true</isExposed>
<masterLabel>CLM Doc Gen Workbench</masterLabel>
<targets>
<target>lightning__RecordAction</target>
<target>lightning__RecordPage</target>
</targets>
<targetConfigs>
<targetConfig targets="lightning__RecordAction">
<actionType>ScreenAction</actionType>
<objects>
<object>Appraiser_Case__c</object>
</objects>
</targetConfig>
<targetConfig targets="lightning__RecordPage">
<objects>
<object>Appraiser_Case__c</object>
</objects>
</targetConfig>
</targetConfigs>
</LightningComponentBundle>

View File

@ -0,0 +1,65 @@
.panel {
display: grid;
gap: 1rem;
padding: 1rem;
}
.controls {
display: grid;
gap: 0.75rem;
}
.button-row {
display: flex;
gap: 0.75rem;
flex-wrap: wrap;
}
.section {
border: 1px solid #d8dde6;
border-radius: 0.5rem;
padding: 1rem;
display: grid;
gap: 0.75rem;
}
.section-title {
font-size: 0.95rem;
font-weight: 700;
margin: 0;
}
.hint {
color: #3e3e3c;
font-size: 0.85rem;
margin: 0;
word-break: break-word;
}
.result {
border-radius: 0.5rem;
padding: 0.75rem 1rem;
}
.result-success {
background: #e8f5e9;
color: #1b5e20;
}
.result-error {
background: #fdecea;
color: #8a1f11;
}
.result-info {
background: #eef4ff;
color: #16325c;
}
.code-block {
margin: 0;
white-space: pre-wrap;
word-break: break-word;
font-size: 0.8rem;
line-height: 1.4;
}

View File

@ -0,0 +1,67 @@
<template>
<lightning-card title="CLM Request Preview" icon-name="standard:snippet">
<div class="panel">
<div class="controls">
<lightning-combobox
label="CLM Account"
value={accountCode}
options={accountOptions}
onchange={handleAccountChange}
disabled={isBusy}
></lightning-combobox>
<lightning-combobox
label="Letter Type"
value={letterCode}
options={letterOptions}
onchange={handleLetterChange}
disabled={isBusy}
></lightning-combobox>
</div>
<div class="button-row">
<lightning-button
label="Refresh Preview"
onclick={refreshAll}
disabled={isBusy}
></lightning-button>
</div>
<template if:true={message}>
<div class={resultClass}>
<p>{message}</p>
</div>
</template>
<template if:true={hasPreview}>
<div class="section">
<h3 class="section-title">Effective Settings</h3>
<p class="hint">Account: {preview.accountDisplayName}</p>
<p class="hint">Letter: {preview.letterDisplayName}</p>
<p class="hint">Template Href: {preview.templateDocHref}</p>
<p class="hint">Destination Folder Href: {preview.destinationFolderHref}</p>
<p class="hint">Destination Filename: {preview.destinationDocName}</p>
</div>
<div class="section">
<h3 class="section-title">Payload JSON</h3>
<pre class="code-block">{preview.payloadJson}</pre>
</div>
<div class="section">
<h3 class="section-title">Data XML</h3>
<pre class="code-block">{preview.dataXml}</pre>
</div>
<div class="section">
<h3 class="section-title">CLM API Endpoint</h3>
<pre class="code-block">POST {preview.mergeTaskEndpointUrl}</pre>
</div>
<div class="section">
<h3 class="section-title">CLM Request Body</h3>
<pre class="code-block">{preview.requestBodyJson}</pre>
</div>
</template>
</div>
</lightning-card>
</template>

View File

@ -0,0 +1,141 @@
import { LightningElement, api } from 'lwc';
import { ShowToastEvent } from 'lightning/platformShowToastEvent';
import getDocGenPreview from '@salesforce/apex/CLMAdminService.getDocGenPreview';
import listAccountSettings from '@salesforce/apex/CLMAdminService.listAccountSettings';
import listLetterSettings from '@salesforce/apex/CLMAdminService.listLetterSettings';
export default class ClmRequestPreview extends LightningElement {
@api recordId;
@api objectApiName;
accountCode = '';
letterCode = '';
accountOptions = [];
letterOptions = [];
preview;
isBusy = false;
message = '';
messageVariant = 'info';
connectedCallback() {
this.initialize();
}
get hasPreview() {
return Boolean(this.preview);
}
get resultClass() {
return `result result-${this.messageVariant}`;
}
async initialize(forceReload = false) {
if (!this.recordId) {
return;
}
this.isBusy = true;
this.clearMessage();
try {
const accounts = await listAccountSettings();
this.accountOptions = (accounts || []).map((account) => ({
label: account.accountDisplayName,
value: account.accountCode
}));
if ((!this.accountCode || forceReload) && this.accountOptions.length > 0) {
this.accountCode = this.accountOptions[0].value;
}
await this.loadLetterOptions(forceReload);
await this.loadPreview();
} catch (error) {
this.handleError(error, 'Unable to initialize CLM preview');
} finally {
this.isBusy = false;
}
}
async loadLetterOptions(forceReload) {
const letters = await listLetterSettings({ accountCode: this.accountCode });
this.letterOptions = (letters || []).map((letter) => ({
label: letter.letterDisplayName,
value: letter.letterCode
}));
const hasExistingSelection = this.letterOptions.some((letter) => letter.value === this.letterCode);
if (!hasExistingSelection || forceReload || !this.letterCode) {
const defaultLetter = (letters || []).find((letter) => letter.isDefault);
this.letterCode = defaultLetter
? defaultLetter.letterCode
: this.letterOptions.length > 0
? this.letterOptions[0].value
: '';
}
}
async loadPreview() {
if (!this.recordId) {
this.showMessage('No record context — place this component on a record page.', 'warning');
return;
}
this.preview = await getDocGenPreview({
appraiserCaseId: this.recordId,
accountCode: this.accountCode,
letterCode: this.letterCode
});
this.showMessage('Preview loaded.', 'success');
}
async handleAccountChange(event) {
this.accountCode = event.detail.value;
await this.refreshAll(true);
}
async handleLetterChange(event) {
this.letterCode = event.detail.value;
await this.refreshAll(false);
}
async refreshAll(forceReloadLetters) {
this.isBusy = true;
this.clearMessage();
try {
if (forceReloadLetters) {
await this.loadLetterOptions(true);
}
await this.loadPreview();
} catch (error) {
this.handleError(error, 'Unable to refresh CLM preview');
} finally {
this.isBusy = false;
}
}
clearMessage() {
this.message = '';
this.messageVariant = 'info';
}
showMessage(message, variant) {
this.message = message;
this.messageVariant = variant;
}
handleError(error, title) {
const bodyMessage = error && error.body ? error.body.message : null;
const directMessage = error ? error.message : null;
const message = bodyMessage || directMessage || 'Unknown error';
this.showMessage(message, 'error');
this.dispatchEvent(
new ShowToastEvent({
title,
message,
variant: 'error'
})
);
}
}

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>63.0</apiVersion>
<isExposed>true</isExposed>
<masterLabel>CLM Request Preview</masterLabel>
<targets>
<target>lightning__RecordPage</target>
</targets>
<targetConfigs>
<targetConfig targets="lightning__RecordPage">
<objects>
<object>Appraiser_Case__c</object>
</objects>
</targetConfig>
</targetConfigs>
</LightningComponentBundle>

View File

@ -0,0 +1,64 @@
.panel {
display: grid;
gap: 1rem;
padding: 1rem;
}
.controls {
display: grid;
gap: 0.75rem;
}
.section {
border: 1px solid #d8dde6;
border-radius: 0.5rem;
padding: 1rem;
display: grid;
gap: 0.75rem;
}
.section-title {
font-size: 0.95rem;
font-weight: 700;
margin: 0;
}
.button-row {
display: flex;
gap: 0.75rem;
flex-wrap: wrap;
}
.hint {
color: #3e3e3c;
font-size: 0.85rem;
margin: 0;
}
.result {
border-radius: 0.5rem;
padding: 0.75rem 1rem;
}
.result-success {
background: #e8f5e9;
color: #1b5e20;
}
.result-error {
background: #fdecea;
color: #8a1f11;
}
.result-info {
background: #eef4ff;
color: #16325c;
}
.json-block {
margin: 0;
white-space: pre-wrap;
word-break: break-word;
font-size: 0.8rem;
line-height: 1.4;
}

View File

@ -0,0 +1,114 @@
<template>
<lightning-card title="Docusign eSignature Workbench" icon-name="standard:contract">
<div class="panel">
<div class="controls">
<lightning-combobox
label="CLM / eSignature Account"
value={accountCode}
options={accountOptions}
onchange={handleAccountChange}
disabled={isBusy}
></lightning-combobox>
<lightning-input
type="date"
label="Envelope From Date"
value={fromDate}
onchange={handleFromDateChange}
disabled={isBusy}
></lightning-input>
</div>
<div class="button-row">
<lightning-button
label="Refresh All"
onclick={refreshData}
disabled={isBusy}
></lightning-button>
<lightning-button
label="Refresh Envelopes"
onclick={refreshEnvelopes}
disabled={isBusy}
></lightning-button>
</div>
<template if:true={hasAccountConfig}>
<div class="section">
<h3 class="section-title">Account Summary</h3>
<p class="hint">Environment: {selectedEnvironment}</p>
<p class="hint">Configured eSignature Account Id: {selectedAccountId}</p>
</div>
</template>
<template if:true={message}>
<div class={resultClass}>
<p>{message}</p>
</div>
</template>
<div class="section">
<h3 class="section-title">Discovered Accounts</h3>
<template if:true={hasAccounts}>
<lightning-datatable
key-field="accountId"
data={accountSummaries}
columns={accountColumns}
hide-checkbox-column
></lightning-datatable>
</template>
<template if:false={hasAccounts}>
<p class="hint">No account discovery data loaded yet.</p>
</template>
</div>
<div class="section">
<h3 class="section-title">Templates</h3>
<template if:true={hasTemplates}>
<lightning-datatable
key-field="templateId"
data={templates}
columns={templateColumns}
hide-checkbox-column
></lightning-datatable>
</template>
<template if:false={hasTemplates}>
<p class="hint">No templates returned for this account.</p>
</template>
</div>
<div class="section">
<h3 class="section-title">Recent Envelopes</h3>
<template if:true={hasEnvelopes}>
<lightning-datatable
key-field="envelopeId"
data={envelopes}
columns={envelopeColumns}
hide-checkbox-column
></lightning-datatable>
</template>
<template if:false={hasEnvelopes}>
<p class="hint">No envelopes returned for this date range.</p>
</template>
</div>
<div class="section">
<h3 class="section-title">Login Information</h3>
<template if:true={hasLoginInfo}>
<pre class="json-block">{loginInfoJson}</pre>
</template>
<template if:false={hasLoginInfo}>
<p class="hint">No login information loaded yet.</p>
</template>
</div>
<div class="section">
<h3 class="section-title">OAuth User Info</h3>
<template if:true={hasUserInfo}>
<pre class="json-block">{userInfoJson}</pre>
</template>
<template if:false={hasUserInfo}>
<p class="hint">No user info loaded yet.</p>
</template>
</div>
</div>
</lightning-card>
</template>

View File

@ -0,0 +1,233 @@
import { LightningElement, api } from 'lwc';
import { ShowToastEvent } from 'lightning/platformShowToastEvent';
import getAccountConfig from '@salesforce/apex/DocusignESignatureService.getAccountConfig';
import getLoginInformation from '@salesforce/apex/DocusignESignatureService.getLoginInformation';
import getUserInfo from '@salesforce/apex/DocusignESignatureService.getUserInfo';
import listAccounts from '@salesforce/apex/DocusignESignatureService.listAccounts';
import listTemplates from '@salesforce/apex/DocusignESignatureService.listTemplates';
import listEnvelopes from '@salesforce/apex/DocusignESignatureService.listEnvelopes';
import listAccountSettings from '@salesforce/apex/CLMAdminService.listAccountSettings';
const TEMPLATE_COLUMNS = [
{ label: 'Template Name', fieldName: 'name', type: 'text' },
{ label: 'Template Id', fieldName: 'templateId', type: 'text' },
{ label: 'Shared', fieldName: 'shared', type: 'text' },
{ label: 'Last Modified', fieldName: 'lastModified', type: 'text' }
];
const ENVELOPE_COLUMNS = [
{ label: 'Subject', fieldName: 'emailSubject', type: 'text' },
{ label: 'Envelope Id', fieldName: 'envelopeId', type: 'text' },
{ label: 'Status', fieldName: 'status', type: 'text' },
{ label: 'Created', fieldName: 'createdDateTime', type: 'text' },
{ label: 'Completed', fieldName: 'completedDateTime', type: 'text' }
];
const ACCOUNT_COLUMNS = [
{ label: 'Account Name', fieldName: 'accountName', type: 'text' },
{ label: 'Account Id', fieldName: 'accountId', type: 'text' },
{ label: 'Base Url', fieldName: 'baseUri', type: 'text' },
{ label: 'Default', fieldName: 'isDefault', type: 'boolean' }
];
export default class DocusignEsignWorkbench extends LightningElement {
@api recordId;
@api objectApiName;
accountOptions = [];
accountCode = '';
accountConfig;
accountSummaries = [];
templates = [];
envelopes = [];
loginInfoJson = '';
userInfoJson = '';
isBusy = false;
message = '';
messageVariant = 'info';
fromDate = this.defaultFromDate();
templateColumns = TEMPLATE_COLUMNS;
envelopeColumns = ENVELOPE_COLUMNS;
accountColumns = ACCOUNT_COLUMNS;
connectedCallback() {
this.initialize();
}
get hasAccountConfig() {
return Boolean(this.accountConfig);
}
get selectedEnvironment() {
return this.accountConfig ? this.accountConfig.environment : '';
}
get selectedAccountId() {
return this.accountConfig ? this.accountConfig.eSignatureAccountId : '';
}
get hasAccounts() {
return this.accountSummaries.length > 0;
}
get hasTemplates() {
return this.templates.length > 0;
}
get hasEnvelopes() {
return this.envelopes.length > 0;
}
get hasLoginInfo() {
return Boolean(this.loginInfoJson);
}
get hasUserInfo() {
return Boolean(this.userInfoJson);
}
get resultClass() {
return `result result-${this.messageVariant}`;
}
async initialize(forceReload = false) {
if (this.isBusy) {
return;
}
this.isBusy = true;
this.clearMessage();
try {
const accounts = await listAccountSettings();
this.accountOptions = (accounts || []).map((account) => ({
label: account.accountDisplayName,
value: account.accountCode
}));
if ((!this.accountCode || forceReload) && this.accountOptions.length > 0) {
this.accountCode = this.accountOptions[0].value;
}
if (!this.accountCode) {
this.showMessage('No active CLM/eSignature accounts are configured.', 'error');
return;
}
await this.loadAll();
} catch (error) {
this.handleError(error, 'Unable to load eSignature workbench');
} finally {
this.isBusy = false;
}
}
async loadAll() {
const [config, accounts, loginInfo, userInfo, templates, envelopes] = await Promise.all([
getAccountConfig({ accountCode: this.accountCode }),
listAccounts({ accountCode: this.accountCode }),
getLoginInformation({ accountCode: this.accountCode }),
getUserInfo({ accountCode: this.accountCode }),
listTemplates({ accountCode: this.accountCode }),
listEnvelopes({ accountCode: this.accountCode, fromDate: this.fromDate })
]);
this.accountConfig = config;
this.accountSummaries = accounts || [];
this.loginInfoJson = this.prettyJson(loginInfo ? loginInfo.responseBody : null);
this.userInfoJson = this.prettyJson(userInfo ? userInfo.responseBody : null);
this.templates = templates || [];
this.envelopes = envelopes || [];
this.showMessage('eSignature data loaded successfully.', 'success');
}
async handleAccountChange(event) {
this.accountCode = event.detail.value;
await this.refreshData();
}
handleFromDateChange(event) {
this.fromDate = event.target.value;
}
async refreshData() {
if (!this.accountCode || this.isBusy) {
return;
}
this.isBusy = true;
this.clearMessage();
try {
await this.loadAll();
} catch (error) {
this.handleError(error, 'Unable to refresh eSignature data');
} finally {
this.isBusy = false;
}
}
async refreshEnvelopes() {
if (!this.accountCode || this.isBusy) {
return;
}
this.isBusy = true;
this.clearMessage();
try {
this.envelopes = await listEnvelopes({ accountCode: this.accountCode, fromDate: this.fromDate });
this.showMessage('Envelope list refreshed.', 'success');
} catch (error) {
this.handleError(error, 'Unable to refresh envelopes');
} finally {
this.isBusy = false;
}
}
defaultFromDate() {
const current = new Date();
current.setDate(current.getDate() - 30);
return current.toISOString().slice(0, 10);
}
prettyJson(raw) {
if (!raw) {
return '';
}
try {
return JSON.stringify(JSON.parse(raw), null, 2);
} catch (error) {
return raw;
}
}
clearMessage() {
this.message = '';
this.messageVariant = 'info';
}
showMessage(message, variant) {
this.message = message;
this.messageVariant = variant;
}
handleError(error, fallbackMessage) {
const message = error && error.body && error.body.message
? error.body.message
: error && error.message
? error.message
: fallbackMessage;
this.showMessage(message, 'error');
this.dispatchEvent(
new ShowToastEvent({
title: 'eSignature Workbench',
message,
variant: 'error'
})
);
}
}

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>63.0</apiVersion>
<isExposed>true</isExposed>
<masterLabel>Docusign eSignature Workbench</masterLabel>
<targets>
<target>lightning__RecordPage</target>
<target>lightning__AppPage</target>
<target>lightning__HomePage</target>
</targets>
<targetConfigs>
<targetConfig targets="lightning__RecordPage">
<objects>
<object>Appraiser_Case__c</object>
</objects>
</targetConfig>
</targetConfigs>
</LightningComponentBundle>

View File

@ -4,21 +4,16 @@
<allowMergeFieldsInHeader>false</allowMergeFieldsInHeader> <allowMergeFieldsInHeader>false</allowMergeFieldsInHeader>
<calloutStatus>Enabled</calloutStatus> <calloutStatus>Enabled</calloutStatus>
<generateAuthorizationHeader>true</generateAuthorizationHeader> <generateAuthorizationHeader>true</generateAuthorizationHeader>
<label>CLMNamedCred</label> <label>AcctDemo_NamedCreds</label>
<namedCredentialParameters> <namedCredentialParameters>
<parameterName>Url</parameterName> <parameterName>Url</parameterName>
<parameterType>Url</parameterType> <parameterType>Url</parameterType>
<parameterValue>https://api.s1.us.clm.demo.docusign.net</parameterValue> <parameterValue>https://account-d.docusign.com</parameterValue>
</namedCredentialParameters> </namedCredentialParameters>
<namedCredentialParameters> <namedCredentialParameters>
<externalCredential>DocusignJWT</externalCredential> <externalCredential>DocusignJWT</externalCredential>
<parameterName>ExternalCredential</parameterName> <parameterName>ExternalCredential</parameterName>
<parameterType>Authentication</parameterType> <parameterType>Authentication</parameterType>
</namedCredentialParameters> </namedCredentialParameters>
<namedCredentialParameters>
<certificate>DocusignJWT</certificate>
<parameterName>ClientCertificate</parameterName>
<parameterType>ClientCertificate</parameterType>
</namedCredentialParameters>
<namedCredentialType>SecuredEndpoint</namedCredentialType> <namedCredentialType>SecuredEndpoint</namedCredentialType>
</NamedCredential> </NamedCredential>

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<NamedCredential xmlns="http://soap.sforce.com/2006/04/metadata">
<allowMergeFieldsInBody>false</allowMergeFieldsInBody>
<allowMergeFieldsInHeader>false</allowMergeFieldsInHeader>
<calloutStatus>Enabled</calloutStatus>
<generateAuthorizationHeader>true</generateAuthorizationHeader>
<label>CLMs1Download</label>
<namedCredentialParameters>
<parameterName>Url</parameterName>
<parameterType>Url</parameterType>
<parameterValue>https://api.s1.us.clm.demo.docusign.net/content</parameterValue>
</namedCredentialParameters>
<namedCredentialParameters>
<externalCredential>DocusignJWT</externalCredential>
<parameterName>ExternalCredential</parameterName>
<parameterType>Authentication</parameterType>
</namedCredentialParameters>
<namedCredentialType>SecuredEndpoint</namedCredentialType>
</NamedCredential>

View File

@ -4,11 +4,11 @@
<allowMergeFieldsInHeader>false</allowMergeFieldsInHeader> <allowMergeFieldsInHeader>false</allowMergeFieldsInHeader>
<calloutStatus>Enabled</calloutStatus> <calloutStatus>Enabled</calloutStatus>
<generateAuthorizationHeader>true</generateAuthorizationHeader> <generateAuthorizationHeader>true</generateAuthorizationHeader>
<label>CLMuatDownloadNamedCreds</label> <label>CLMs1NamedCreds</label>
<namedCredentialParameters> <namedCredentialParameters>
<parameterName>Url</parameterName> <parameterName>Url</parameterName>
<parameterType>Url</parameterType> <parameterType>Url</parameterType>
<parameterValue>https://apidownloaduatna11.springcm.com</parameterValue> <parameterValue>https://api.s1.us.clm.demo.docusign.net</parameterValue>
</namedCredentialParameters> </namedCredentialParameters>
<namedCredentialParameters> <namedCredentialParameters>
<externalCredential>DocusignJWT</externalCredential> <externalCredential>DocusignJWT</externalCredential>

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<NamedCredential xmlns="http://soap.sforce.com/2006/04/metadata">
<allowMergeFieldsInBody>false</allowMergeFieldsInBody>
<allowMergeFieldsInHeader>false</allowMergeFieldsInHeader>
<calloutStatus>Enabled</calloutStatus>
<generateAuthorizationHeader>true</generateAuthorizationHeader>
<label>Esignature_Demo_NamedCreds</label>
<namedCredentialParameters>
<parameterName>Url</parameterName>
<parameterType>Url</parameterType>
<parameterValue>https://demo.docusign.net/restapi</parameterValue>
</namedCredentialParameters>
<namedCredentialParameters>
<externalCredential>DocusignJWT</externalCredential>
<parameterName>ExternalCredential</parameterName>
<parameterType>Authentication</parameterType>
</namedCredentialParameters>
<namedCredentialType>SecuredEndpoint</namedCredentialType>
</NamedCredential>

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>Reference__c</fullName>
<externalId>false</externalId>
<label>Reference</label>
<length>255</length>
<required>false</required>
<trackHistory>false</trackHistory>
<type>Text</type>
<unique>false</unique>
</CustomField>

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<ValidationRule xmlns="http://soap.sforce.com/2006/04/metadata">
<active>true</active>
<description>Prevents empty or incomplete deficiency rows from being saved.</description>
<fullName>Required_Deficiency_Data</fullName>
<errorConditionFormula>OR(
ISBLANK(TEXT(Deficiency_Number__c)),
ISBLANK(Description__c),
ISBLANK(Resolution__c)
)</errorConditionFormula>
<errorDisplayField>Deficiency_Number__c</errorDisplayField>
<errorMessage>Deficiency Number, Description, and Resolution are required.</errorMessage>
</ValidationRule>

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>Appraiser_City__c</fullName>
<externalId>false</externalId>
<label>Appraiser City</label>
<length>255</length>
<required>false</required>
<trackHistory>false</trackHistory>
<type>Text</type>
<unique>false</unique>
</CustomField>

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>Appraiser_Country__c</fullName>
<externalId>false</externalId>
<label>Appraiser Country</label>
<length>255</length>
<required>false</required>
<trackHistory>false</trackHistory>
<type>Text</type>
<unique>false</unique>
</CustomField>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>Appraiser_Email__c</fullName>
<externalId>false</externalId>
<label>Appraiser Email</label>
<required>false</required>
<trackHistory>false</trackHistory>
<type>Email</type>
<unique>false</unique>
</CustomField>

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>Appraiser_Last_Name__c</fullName>
<externalId>false</externalId>
<label>Appraiser Last Name</label>
<length>255</length>
<required>false</required>
<trackHistory>false</trackHistory>
<type>Text</type>
<unique>false</unique>
</CustomField>

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>Appraiser_Name__c</fullName>
<externalId>false</externalId>
<label>Appraiser Name</label>
<length>255</length>
<required>false</required>
<trackHistory>false</trackHistory>
<type>Text</type>
<unique>false</unique>
</CustomField>

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>Appraiser_Postal_Code__c</fullName>
<externalId>false</externalId>
<label>Appraiser Postal Code</label>
<length>40</length>
<required>false</required>
<trackHistory>false</trackHistory>
<type>Text</type>
<unique>false</unique>
</CustomField>

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>Appraiser_Salutation__c</fullName>
<externalId>false</externalId>
<label>Appraiser Salutation</label>
<length>10</length>
<required>false</required>
<trackHistory>false</trackHistory>
<type>Text</type>
<unique>false</unique>
</CustomField>

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>Appraiser_State_Province__c</fullName>
<externalId>false</externalId>
<label>Appraiser State/Province</label>
<length>255</length>
<required>false</required>
<trackHistory>false</trackHistory>
<type>Text</type>
<unique>false</unique>
</CustomField>

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>Appraiser_Street__c</fullName>
<externalId>false</externalId>
<label>Appraiser Street</label>
<length>255</length>
<required>false</required>
<trackHistory>false</trackHistory>
<type>Text</type>
<unique>false</unique>
</CustomField>

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>Attached_File_Content_Document_Id__c</fullName>
<externalId>false</externalId>
<label>Attached File Content Document Id</label>
<length>18</length>
<required>false</required>
<trackHistory>false</trackHistory>
<type>Text</type>
<unique>false</unique>
</CustomField>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>Attached_File_Url__c</fullName>
<externalId>false</externalId>
<label>Attached File Url</label>
<required>false</required>
<trackHistory>false</trackHistory>
<type>Url</type>
</CustomField>

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>FHA_Case_Number__c</fullName>
<externalId>false</externalId>
<label>FHA Case Number</label>
<length>40</length>
<required>false</required>
<trackHistory>false</trackHistory>
<type>Text</type>
<unique>false</unique>
</CustomField>

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>Generated_Document_Id__c</fullName>
<externalId>false</externalId>
<label>Generated Document Id</label>
<length>255</length>
<required>false</required>
<trackHistory>false</trackHistory>
<type>Text</type>
<unique>false</unique>
</CustomField>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>Generated_Document_Url__c</fullName>
<externalId>false</externalId>
<label>Generated Document Url</label>
<required>false</required>
<trackHistory>false</trackHistory>
<type>Url</type>
</CustomField>

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>Last_CLM_Account_Code__c</fullName>
<externalId>false</externalId>
<label>Last CLM Account Code</label>
<length>80</length>
<required>false</required>
<trackHistory>false</trackHistory>
<type>Text</type>
<unique>false</unique>
</CustomField>

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>Last_Destination_Folder_Href__c</fullName>
<externalId>false</externalId>
<label>Last Destination Folder Href</label>
<length>255</length>
<required>false</required>
<trackHistory>false</trackHistory>
<type>Text</type>
<unique>false</unique>
</CustomField>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>Last_DocGen_Completed_At__c</fullName>
<externalId>false</externalId>
<label>Last DocGen Completed At</label>
<required>false</required>
<trackHistory>false</trackHistory>
<type>DateTime</type>
</CustomField>

View File

@ -1,10 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata"> <CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>Description__c</fullName> <fullName>Last_DocGen_Message__c</fullName>
<label>Description</label> <externalId>false</externalId>
<label>Last DocGen Message</label>
<length>32768</length> <length>32768</length>
<required>false</required> <required>false</required>
<trackHistory>true</trackHistory>
<type>LongTextArea</type> <type>LongTextArea</type>
<visibleLines>5</visibleLines> <visibleLines>3</visibleLines>
</CustomField> </CustomField>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>Last_DocGen_Requested_At__c</fullName>
<externalId>false</externalId>
<label>Last DocGen Requested At</label>
<required>false</required>
<trackHistory>false</trackHistory>
<type>DateTime</type>
</CustomField>

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>Last_DocGen_Status__c</fullName>
<externalId>false</externalId>
<label>Last DocGen Status</label>
<length>100</length>
<required>false</required>
<trackHistory>false</trackHistory>
<type>Text</type>
<unique>false</unique>
</CustomField>

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>Last_DocGen_Task_Id__c</fullName>
<externalId>false</externalId>
<label>Last DocGen Task Id</label>
<length>255</length>
<required>false</required>
<trackHistory>false</trackHistory>
<type>Text</type>
<unique>false</unique>
</CustomField>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>Last_DocGen_Task_Url__c</fullName>
<externalId>false</externalId>
<label>Last DocGen Task Url</label>
<required>false</required>
<trackHistory>false</trackHistory>
<type>Url</type>
</CustomField>

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>Last_Template_Document_Href__c</fullName>
<externalId>false</externalId>
<label>Last Template Document Href</label>
<length>255</length>
<required>false</required>
<trackHistory>false</trackHistory>
<type>Text</type>
<unique>false</unique>
</CustomField>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>Letter_Sent_Date__c</fullName>
<externalId>false</externalId>
<label>Letter Sent Date</label>
<required>false</required>
<trackHistory>false</trackHistory>
<type>Date</type>
</CustomField>

View File

@ -1,12 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<ListView xmlns="http://soap.sforce.com/2006/04/metadata"> <ListView xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>All</fullName> <fullName>All</fullName>
<columns>Name</columns>
<columns>Appraiser_Field_Review_Date__c</columns> <columns>Appraiser_Field_Review_Date__c</columns>
<columns>Property_Street__c</columns> <columns>Property_Street__c</columns>
<columns>Property_City__c</columns> <columns>Property_City__c</columns>
<columns>Property_State_Province__c</columns> <columns>Property_State_Province__c</columns>
<columns>LastModifiedDate</columns>
<filterScope>Everything</filterScope> <filterScope>Everything</filterScope>
<label>All</label> <label>All</label>
</ListView> </ListView>

View File

@ -1,37 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<CustomObject xmlns="http://soap.sforce.com/2006/04/metadata">
<actionOverrides>
<actionName>New</actionName>
<type>Default</type>
</actionOverrides>
<actionOverrides>
<actionName>Edit</actionName>
<type>Default</type>
</actionOverrides>
<actionOverrides>
<actionName>View</actionName>
<type>Default</type>
</actionOverrides>
<allowInChatterGroups>false</allowInChatterGroups>
<compactLayoutAssignment>SYSTEM</compactLayoutAssignment>
<deploymentStatus>Deployed</deploymentStatus>
<enableActivities>false</enableActivities>
<enableBulkApi>true</enableBulkApi>
<enableFeeds>false</enableFeeds>
<enableHistory>true</enableHistory>
<enableLicensing>false</enableLicensing>
<enableReports>true</enableReports>
<enableSearch>true</enableSearch>
<enableSharing>true</enableSharing>
<enableStreamingApi>true</enableStreamingApi>
<externalSharingModel>ControlledByParent</externalSharingModel>
<label>Appraiser Deficiency</label>
<nameField>
<label>Appraiser Deficiency Name</label>
<type>Text</type>
</nameField>
<pluralLabel>Appraiser Deficiencies</pluralLabel>
<searchLayouts></searchLayouts>
<sharingModel>ControlledByParent</sharingModel>
<visibility>Public</visibility>
</CustomObject>

View File

@ -1,13 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>Appraiser_Case__c</fullName>
<label>Appraiser Case</label>
<referenceTo>Appraiser_Case__c</referenceTo>
<relationshipLabel>Appraiser Deficiencies</relationshipLabel>
<relationshipName>Appraiser_Deficiencies</relationshipName>
<reparentableMasterDetail>false</reparentableMasterDetail>
<required>true</required>
<trackHistory>false</trackHistory>
<type>MasterDetail</type>
<writeRequiresMasterRead>false</writeRequiresMasterRead>
</CustomField>

View File

@ -1,9 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>Deficiency_Number__c</fullName>
<label>Deficiency Number</label>
<length>50</length>
<required>false</required>
<trackHistory>true</trackHistory>
<type>Text</type>
</CustomField>

Some files were not shown because too many files have changed in this diff Show More