Compare commits
13 Commits
e058dedb82
...
66be5d83ff
| Author | SHA1 | Date |
|---|---|---|
|
|
66be5d83ff | |
|
|
82e10ff810 | |
|
|
b1c199d21d | |
|
|
c49d127db1 | |
|
|
546ffd7dcf | |
|
|
8fb9df01db | |
|
|
091b1870b6 | |
|
|
148854ff1e | |
|
|
0ce516bbfc | |
|
|
fe337efe63 | |
|
|
45814dc2d5 | |
|
|
62b78faf1a | |
|
|
703fb0c0ba |
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
@ -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._
|
||||||
|
|
@ -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_
|
||||||
|
|
@ -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_
|
||||||
|
|
@ -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;
|
||||||
CLMDocGenCallout.CLMDocGenResponse response = CLMDocGenCallout.generateDocument(
|
private String destinationFolderHref;
|
||||||
record.Id,
|
private String destinationDocName;
|
||||||
'TEMPLATE_ID_FROM_CLM', // e.g., '123456'
|
|
||||||
record.Reviewer_Email__c
|
public AppraiserCaseDocGenJob(Id caseId, String accountCode,
|
||||||
);
|
String templateDocHref, String destinationFolderHref, String destinationDocName) {
|
||||||
|
this.caseId = caseId;
|
||||||
if (!response.success) {
|
this.accountCode = accountCode;
|
||||||
// Log error or send notification
|
this.templateDocHref = templateDocHref;
|
||||||
System.debug('CLM Doc Gen failed: ' + response.message);
|
this.destinationFolderHref = destinationFolderHref;
|
||||||
|
this.destinationDocName = destinationDocName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void execute(QueueableContext ctx) {
|
||||||
|
CLMDocGenCallout.CLMDocGenResponse response = CLMAdminService.generateDocument(
|
||||||
|
caseId, templateDocHref, destinationFolderHref, destinationDocName, accountCode
|
||||||
|
);
|
||||||
|
if (!response.success) {
|
||||||
|
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}}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
@ -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._
|
|
||||||
|
|
|
||||||
|
|
@ -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 don’t 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_
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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._
|
|
||||||
|
|
|
||||||
|
|
@ -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”
|
||||||
|
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -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._
|
|
||||||
|
|
|
||||||
|
|
@ -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._
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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, ', ');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,18 +1,95 @@
|
||||||
public class CLMDocGenCallout {
|
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,
|
||||||
|
templateDocHref,
|
||||||
|
destinationFolderHref,
|
||||||
|
destinationDocName,
|
||||||
|
env,
|
||||||
|
defaultAccountId(env),
|
||||||
|
clmNamedCredential(env).substringAfter('callout:')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Map<String, Object> requestBody = new Map<String, Object>{
|
public static CLMDocGenResponse generateDocument(
|
||||||
'TemplateDocument' => new Map<String, Object>{
|
String caseId,
|
||||||
'Href' => templateDocHref
|
String templateDocHref,
|
||||||
},
|
String destinationFolderHref,
|
||||||
'DataXML' => buildDataXml(casePayload),
|
String destinationDocName,
|
||||||
'DestinationDocumentName' => destinationDocName,
|
String env,
|
||||||
'DestinationFolder' => new Map<String, Object>{
|
String configuredAccountId,
|
||||||
'Href' => destinationFolderHref
|
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('&', '&')
|
return s.replace('&', '&')
|
||||||
|
|
@ -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 = success;
|
this(success, message, documentUrl, documentId, null, null, null, null);
|
||||||
this.message = message;
|
}
|
||||||
|
|
||||||
|
public CLMDocGenResponse(
|
||||||
|
Boolean success,
|
||||||
|
String message,
|
||||||
|
String documentUrl,
|
||||||
|
String documentId,
|
||||||
|
String taskStatus,
|
||||||
|
String generatedDocumentUrl,
|
||||||
|
String generatedDocumentId,
|
||||||
|
String taskDetailsJson
|
||||||
|
) {
|
||||||
|
this.success = success;
|
||||||
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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('&'));
|
||||||
|
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'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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'
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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'
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
|
||||||
|
|
@ -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>
|
|
||||||
|
|
@ -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
Loading…
Reference in New Issue