Initial commit: Salesforce Appraiser Review Letter with DocuSign CLM integration

This commit is contained in:
paulh 2026-04-03 12:13:59 -04:00
commit 63b1bfd758
30 changed files with 1390 additions and 0 deletions

17
.gitignore vendored Normal file
View File

@ -0,0 +1,17 @@
# Salesforce / SFDX
.sfdx/
.sf/
*.lock
# Node / npm
node_modules/
package-lock.json
# Python
__pycache__/
*.pyc
.env
# OS
.DS_Store
Thumbs.db

Binary file not shown.

323
docs/CLM_INTEGRATION.md Normal file
View File

@ -0,0 +1,323 @@
# CLM Doc Gen Integration Guide
## Overview
This guide explains how to integrate Salesforce with DocuSign CLM to generate Appraiser Review Letters dynamically from Appraiser Case records.
---
## Architecture
### Components
1. **Salesforce Objects**: Appraiser_Case__c and Appraiser_Case_Deficiency__c
2. **Apex Payload Builder**: `AppraiserCasePayloadBuilder` - transforms SOQL data into CLM merge format
3. **HTTP Callout**: `CLMDocGenCallout` - invokes DocuSign CLM API
4. **CLM Template**: DocuSign CLM-hosted template with merge fields and repeat blocks
5. **Flow or Apex Trigger**: Orchestrates when/how document generation happens
### Data Flow
```
Appraiser Case (UI/API)
Salesforce Apex/Flow
AppraiserCasePayloadBuilder.buildPayload() [builds merge data]
CLMDocGenCallout.generateDocument() [sends to CLM API]
DocuSign CLM
Generated PDF + Envelope
Recipient Email
```
---
## Setup Steps
### 1. Configure Named Credentials (Salesforce)
**Goal**: Store CLM API endpoint and authentication credentials securely.
1. Go to Setup → Named Credentials → New
2. Configure:
- Label: `CLMNamedCred`
- URL: `https://[your-clm-instance].docusign.com`
- Authentication Protocol: `OAuth 2.0`
- Client ID: (from DocuSign CLM admin console)
- Client Secret: (from DocuSign CLM admin console)
- Scope: (typically `signature`)
- Token Endpoint: `https://[your-clm-instance].docusign.com/oauth/token`
3. Save
**Alternative**: For testing, use a custom Named Credential with API Key auth if available from your CLM admin.
### 2. Configure Remote Site Settings (Salesforce)
**Goal**: Whitelist CLM domain for HTTP callouts.
1. Go to Setup → Remote Site Settings → New
2. Configure:
- Remote Site Name: `DocuSignCLM`
- Remote Site URL: `https://[your-clm-instance].docusign.com`
- Disable Protocol Security: (unchecked for production)
3. Save
### 3. Get CLM Template ID
**Goal**: Identify which CLM template to use for Appraiser Review Letters.
1. In DocuSign CLM admin console, navigate to Templates
2. Find or create the Appraiser Review Letter template
3. Note the Template ID (usually a UUID or numeric string)
4. Verify the template expects these merge fields:
- `AppraiserCaseNumber` (Text)
- `AppraiserFieldReviewDate` (Date)
- `PropertyAddress` (Text)
- `DeficiencyList[]` (Array/Lines table with deficiencyNumber, description, resolution)
---
## Usage Patterns
### Pattern 1: Apex Trigger (Automatic)
**Scenario**: Generate letter automatically when Appraiser Case status reaches "Ready for Review"
```apex
// In a trigger on Appraiser_Case__c AFTER UPDATE
if (oldMap.get(record.Id).Status__c != 'Ready for Review' &&
record.Status__c == 'Ready for Review') {
CLMDocGenCallout.CLMDocGenResponse response = CLMDocGenCallout.generateDocument(
record.Id,
'TEMPLATE_ID_FROM_CLM', // e.g., '123456'
record.Reviewer_Email__c
);
if (!response.success) {
// Log error or send notification
System.debug('CLM Doc Gen failed: ' + response.message);
}
}
```
### Pattern 2: Flow (UI-Driven)
**Scenario**: User clicks button to generate letter on-demand
1. Create a Record-Triggered Flow on Appraiser_Case__c
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)
**Scenario**: External system calls Salesforce to generate letter
```apex
@RestResource(urlMapping='/appraiser-case-generate-letter')
global class AppraiserCaseDocGenRest {
@HttpPost
global static void generateLetter(String caseId, String templateId, String recipientEmail) {
CLMDocGenCallout.CLMDocGenResponse response = CLMDocGenCallout.generateDocument(
caseId, templateId, recipientEmail
);
// Return response
RestContext.response.statusCode = response.success ? 200 : 400;
RestContext.response.responseBody = Blob.valueOf(JSON.serialize(response));
}
}
```
---
## Payload Structure
### Input
```json
{
"AppraiserCaseNumber": "AC-00001",
"AppraiserFieldReviewDate": "2026-04-02",
"PropertyAddress": "123 Main St, Denver, CO 80202",
"DeficiencyList": [
{
"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)
```json
{
"templateId": "TEMPLATE_ID_FROM_CLM",
"mergeData": { ...payload above... },
"delivery": {
"recipientEmail": "reviewer@example.com",
"documentName": "AppraiserReviewLetter_1743724800000"
},
"metadata": {
"salesforceRecordId": "a0wKW000007OIiCYAW",
"generatedAt": "2026-04-02T05:27:44Z"
}
}
```
### CLM API Response (on success)
```json
{
"success": true,
"documentUrl": "https://clm-instance.docusign.com/documents/ABC123XYZ",
"documentId": "DOC-001",
"message": "Document generated successfully"
}
```
---
## CLM Template Design
### Template Merge Tags (Handlebars syntax)
**Flat fields**:
```handlebars
<p>Case Number: {{AppraiserCaseNumber}}</p>
<p>Review Date: {{AppraiserFieldReviewDate}}</p>
<p>Property: {{PropertyAddress}}</p>
```
**Deficiency repeat block**:
```handlebars
<table>
<tr>
<th>Deficiency #</th>
<th>Description</th>
<th>Resolution</th>
</tr>
{{#each DeficiencyList}}
<tr>
<td>{{deficiencyNumber}}</td>
<td>{{description}}</td>
<td>{{resolution}}</td>
</tr>
{{/each}}
</table>
```
**Conditional (if no deficiencies)**:
```handlebars
{{#if DeficiencyList.length}}
<!-- Deficiency table -->
{{else}}
<p>No deficiencies found.</p>
{{/if}}
```
---
## Testing
### Unit Test (Apex)
```bash
sf apex run test --test-level RunLocalTests --target-org appraiser-dev
```
Expected: AppraiserCasePayloadBuilderTest passes all assertions.
### Integration Test (Manual)
1. In Salesforce, create an Appraiser Case with 2-3 sample deficiencies
2. Run (in Apex Execute):
```apex
String caseId = 'a0wKW000007OIiCYAW';
Map<String, Object> payload = AppraiserCasePayloadBuilder.buildPayload(caseId);
System.debug(JSON.serialize(payload));
```
3. Copy payload output
4. Verify all fields and DeficiencyList array are populated
### CLM Integration Test
1. Set up Named Credentials and Remote Site Settings (see Setup section)
2. Configure CLM template ID in CLMDocGenCallout
3. Run (in Apex Execute):
```apex
String caseId = 'a0wKW000007OIiCYAW';
String templateId = 'TEMPLATE_123';
String recipientEmail = 'test@example.com';
CLMDocGenCallout.CLMDocGenResponse response = CLMDocGenCallout.generateDocument(
caseId, templateId, recipientEmail
);
System.debug('Success: ' + response.success);
System.debug('Message: ' + response.message);
System.debug('Document URL: ' + response.documentUrl);
```
4. Monitor CLM instance for outbound document delivery
---
## Troubleshooting
### "Document generated successfully" but no email received
- Check recipient email in CLM settings (delivery rules may have delay)
- Verify Email-to-Sign integration is enabled in CLM
- Check CLM audit log for delivery status
### HTTP 401 Unauthorized (Named Credentials)
- Verify OAuth token is valid in Named Credentials
- Refresh token or re-authorize
- Check OAuth scope matches CLM permissions
### "Appraiser Case not found" error
- Verify record ID is correct
- Ensure Appraiser_Case__c object permissions are granted to running user
### Empty DeficiencyList in generated document
- Check that related Appraiser_Case_Deficiency__c records exist
- Verify CLM template correctly references {{DeficiencyList}}
- Test payload in Apex to confirm array is populated
---
## Performance Notes
- `AppraiserCasePayloadBuilder.buildPayload()` runs one query (with related records in subquery)
- `CLMDocGenCallout.generateDocument()` performs one HTTP callout (blocks execution ~1-5 seconds)
- For bulk operations, consider queueable jobs or batch class to manage API rate limits
---
## Next Steps
1. ✅ Deploy Apex classes to org (included in manifest)
2. Configure Named Credentials with CLM OAuth/API credentials
3. Add CLM Template ID to CLMDocGenCallout (configurable constant or custom setting)
4. Build a Flow or Trigger to invoke CLMDocGenCallout
5. Test end-to-end with sample Appraiser Case records
---
_Last updated: 2026-04-02_
_Updated to include setup instructions and integration patterns._

View File

@ -0,0 +1,89 @@
# CLM Template Guide — Appraiser Review Letter
## Overview
This guide explains how to structure, configure, and manage Appraiser Review Letter templates within Salesforce CLM. It covers:
- Repeat/array merge tags
- Conditional visibility (show/hide questions, sections, deficiencies)
- Table and paragraph formatting
- Edge cases (null values, formatting challenges, deficiencies)
- Example usage patterns
- Decision and action points (explicit issues or questions to resolve)
---
## Merge Tag Techniques
- Table/block repeats: {{#reviewQuestions}} ... {{/reviewQuestions}}
- Paragraphs vs. tables, hiding/showing blocks
## Common Scenarios
- Including only answered questions
- Conditional: show deficiency section/flag if relevant
## Repeat/Array Tags
### Usage Example:
```handlebars
{{#each DeficiencyList}}
<tr>
<td>{{DeficiencyType}}</td>
<td>{{DeficiencyDescription}}</td>
</tr>
{{/each}}
```
- Use `each` blocks to render dynamic content (arrays/lists from Salesforce).
- Supports variable-length tables and grouped paragraphs.
## Conditional Sections
Example:
```handlebars
{{#if IsDeficiency}}
Section: Deficiencies Found
{{else}}
Section: No Deficiencies
{{/if}}
```
- Show/hide based on merge fields (boolean or enumerated).
- Can target entire sections, paragraphs, or individual questions.
## Table/Paragraph Examples
**Deficiency Table:**
```html
<table>
<thead>
<tr><th>Type</th><th>Description</th></tr>
</thead>
<tbody>
<!-- Dynamic rows go here -->
{{#each DeficiencyList}}
<tr><td>{{DeficiencyType}}</td><td>{{DeficiencyDescription}}</td></tr>
{{/each}}
</tbody>
</table>
```
**Paragraph Blocks:**
```handlebars
{{#each Comments}}
<p>{{CommentText}}</p>
{{/each}}
```
## Edge Case Handling
- If `DeficiencyList` is empty/null, render a fallback paragraph: `No deficiencies found.`
- Fields may be string, number, or boolean—always sanitize output in template.
- Format sections to avoid unwanted whitespace/empty tables.
## Questions to Clarify
- What is the complete list of merge fields expected for each template?
- Are custom data transformations needed before merge?
- Are any fields multi-select or nested objects?
- How are null/empty values handled (leave blank vs. explicit text)?
---
_Last updated: 2026-02-26 10:32 AM_
_Work in progress: More examples and Salesforce integration notes coming next._

View File

@ -0,0 +1,82 @@
# Deployment & Testing — Appraiser Review Letter
## Implemented Salesforce Metadata
- Parent object: Appraiser_Case__c (label: Appraiser Case)
- Name field (auto number): Appraiser Case Number (AC-{00000})
- Fields on Appraiser Case:
- Appraiser_Field_Review_Date__c (Date)
- Property_Address__c (Text 255)
- Child object for repeatable deficiencies: Appraiser_Case_Deficiency__c
- Fields on Appraiser Case Deficiency:
- Appraiser_Case__c (Master-Detail to Appraiser_Case__c)
- Deficiency_Number__c (Number)
- Description__c (Long Text Area)
- Resolution__c (Long Text Area)
- Permission set: Appraiser_Case_Access
- Apex Classes:
- AppraiserCasePayloadBuilder: Transforms Salesforce data to CLM merge payload
- AppraiserCasePayloadBuilderTest: Unit tests for payload builder
- CLMDocGenCallout: HTTP integration with DocuSign CLM API
## Deployment Steps
1. Deploy custom objects & fields
2. Deploy Apex classes (included in manifest)
3. Configure Named Credentials for CLM API access
4. Configure Remote Site Settings for CLM instance
5. Map merge fields to object schema
6. Configure CLM template and connect API
7. Test with sample Appraiser Case records
### Suggested CLI Deploy Commands
1. Authenticate to your org:
- sf org login web --alias appraiser-dev
2. Validate source deploy:
- sf project deploy validate --target-org appraiser-dev --manifest manifest/package.xml
3. Deploy metadata:
- sf project deploy start --target-org appraiser-dev --manifest manifest/package.xml
---
## Testing Workflow
- Use sample JSON payloads (see requirements.md)
- Validate conditional logic—test both existence and missing data
- Test table rendering for arrays (DeficiencyList, ReviewerComments)
- Document any failed merges or formatting gaps
## CLM Data Mapping Starter
- AppraiserCaseNumber -> Appraiser_Case__c.Name
- AppraiserFieldReviewDate -> Appraiser_Case__c.Appraiser_Field_Review_Date__c
- PropertyAddress -> Appraiser_Case__c.Property_Address__c
- DeficiencyList[] -> Appraiser_Case__c.Deficiencies__r
- deficiencyNumber -> Deficiency_Number__c
- description -> Description__c
- resolution -> Resolution__c
## Smoke Test Execution
After deployment, sample test data was created:
- Appraiser Case: AC-00001 (a0wKW000007OIiCYAW)
- 3 related deficiency records verified
### Test Payload Query (run in Apex Execute or Query Editor)
```soql
SELECT Id, Name, Appraiser_Field_Review_Date__c, Property_Address__c,
(SELECT Id, Deficiency_Number__c, Description__c, Resolution__c
FROM Deficiencies__r ORDER BY Deficiency_Number__c)
FROM Appraiser_Case__c
WHERE Id='a0wKW000007OIiCYAW'
```
### Test Payload Generation (Apex)
```apex
String caseId = 'a0wKW000007OIiCYAW';
Map<String, Object> payload = AppraiserCasePayloadBuilder.buildPayload(caseId);
System.debug(JSON.serializePretty(payload));
```
## CLM Doc Gen Integration
See [CLM_INTEGRATION.md](CLM_INTEGRATION.md) for complete setup and integration patterns.
---
_Last updated: 2026-02-26 13:26 PM_
_Work in progress: Add test cases and troubleshooting checklist._

19
docs/FEATURES_UPDATE.md Normal file
View File

@ -0,0 +1,19 @@
# Features/Change Log — Appraiser Review Letter
## Progress Summary
- 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
- README.md: Project intro and architecture brief
- DEPLOYMENT_AND_TESTING.md: Deployment steps and testing workflow drafted
---
## Next Steps
- Expand template engine features (nested conditionals, richer formatting)
- Clarify integration specifics with Salesforce CLM
- Add more actionable questions in doc footers
---
_Last updated: 2026-02-26 13:28 PM_
_Work in progress: Feature/requirement expansion and blockages/questions to be listed here._

28
docs/README.md Normal file
View File

@ -0,0 +1,28 @@
# 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
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.
## Architecture Overview
- Templates are rendered using dynamic data from Salesforce objects
- All merge fields and arrays are mapped from Salesforce data model
- Modular blocks for easy maintenance and expansion
## Key Features
- Dynamic merge of Appraiser Review answers (tables, paragraphs)
- Salesforce-initiated CLM document generation and delivery
- Robust requirements, data, and design documentation
## Onboarding
- Clone docs directory into your Salesforce project repo
- 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_
_Work in progress: Add quick-start workflow and test recommendations._

45
docs/design.md Normal file
View File

@ -0,0 +1,45 @@
# Design — Appraiser Review Letter Generator
## Architecture
Describe the template structure, merge field handling logic, and integration with Salesforce CLM.
---
## 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._

30
docs/document-plan.md Normal file
View File

@ -0,0 +1,30 @@
# Documentation Plan — salesforce-appraiser-review-letter
**Purpose:** Lay out the set of core documents and their roles for the Appraiser Review Letter workflow (Salesforce + DocuSign CLM).
---
## 1. README.md
- **Overview:** High-level system explanation, architecture, quick start
## 2. requirements.md
- **Functional and Non-Functional Requirements**
- **Business rules, user stories, field mapping**
## 3. design.md
- **Data Model Design:** Objects, relationships, datatypes
- **Integration:** Merge data JSON, API spec
- **CLM Template:** Merge tag/logic guide, how variable content/tables are filled
## 4. CLM_TEMPLATE_GUIDE.md
- **Details for CLM Template Admins:** Merge tag syntax, dynamic block/table examples, versioning strategy
## 5. DEPLOYMENT_AND_TESTING.md
- **Install and Configure:** Project setup, org/CLM config, verification checklist, test plan
## 6. FEATURES_UPDATE.md
- **Change Log/Features:** Major improvements, new capabilities, key learnings
---
**You can add more docs as needed (e.g., UX mocks, API payload samples, FAQ). This plan keeps reference and handoff friction low.**

80
docs/requirements.md Normal file
View File

@ -0,0 +1,80 @@
# Requirements — Appraiser Review Letter Generator
## Purpose
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
- 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)?
## Initial Authoritative Salesforce Schema
- Appraiser_Case__c (Appraiser Case)
- Name (label: Appraiser Case Number, AutoNumber)
- Appraiser_Field_Review_Date__c (Date)
- Property_Address__c (Text)
- Appraiser_Case_Deficiency__c (Appraiser Case Deficiency)
- Appraiser_Case__c (Master-Detail -> Appraiser_Case__c)
- Deficiency_Number__c (Number)
- Description__c (Long Text Area)
- Resolution__c (Long Text Area)
This schema supports CLM array merges by iterating child deficiency records tied to one appraiser case.
## CLM Integration
### Payload Structure (from AppraiserCasePayloadBuilder)
The Apex class `AppraiserCasePayloadBuilder` transforms Salesforce records into CLM-ready JSON:
- AppraiserCaseNumber (string) -> Appraiser_Case__c.Name
- AppraiserFieldReviewDate (ISO date) -> Appraiser_Case__c.Appraiser_Field_Review_Date__c
- PropertyAddress (string) -> Appraiser_Case__c.Property_Address__c
- DeficiencyList (array of objects):
- deficiencyNumber (number) -> Appraiser_Case_Deficiency__c.Deficiency_Number__c
- description (string) -> Appraiser_Case_Deficiency__c.Description__c
- resolution (string) -> Appraiser_Case_Deficiency__c.Resolution__c
### CLM API Integration (CLMDocGenCallout)
- HTTP POST to DocuSign CLM API with merge payload
- Named Credentials: Securely store CLM endpoint + OAuth token
- Remote Site Settings: Whitelist CLM instance domain
- Response includes document URL and ID for tracking
See [CLM_INTEGRATION.md](CLM_INTEGRATION.md) for setup and usage patterns.
---
_Last updated: 2026-02-26 10:35 AM_
_Work in progress: Integration and schema expansion next._

View File

@ -0,0 +1,83 @@
/**
* @description Builds CLM-ready JSON payload for Appraiser Case with related Deficiencies.
* Used to transform Salesforce data into DocuSign CLM merge field structure.
*/
public class AppraiserCasePayloadBuilder {
/**
* @description Generates CLM merge payload for a given Appraiser Case.
* @param caseId Appraiser_Case__c record Id
* @return Map<String, Object> CLM merge data ready for template rendering
*/
public static Map<String, Object> buildPayload(String caseId) {
// Query parent case with all child deficiencies
Appraiser_Case__c appraiserCase = queryAppraiserCase(caseId);
if (appraiserCase == null) {
throw new IllegalArgumentException('Appraiser Case not found: ' + caseId);
}
// Build CLM payload structure
Map<String, Object> payload = new Map<String, Object>();
payload.put('AppraiserCaseNumber', appraiserCase.Name);
payload.put('AppraiserFieldReviewDate', formatDate(appraiserCase.Appraiser_Field_Review_Date__c));
payload.put('PropertyAddress', appraiserCase.Property_Address__c);
// Transform child deficiencies into DeficiencyList array
List<Map<String, Object>> deficiencyList = new List<Map<String, Object>>();
if (appraiserCase.Deficiencies__r != null && !appraiserCase.Deficiencies__r.isEmpty()) {
for (Appraiser_Case_Deficiency__c deficiency : appraiserCase.Deficiencies__r) {
Map<String, Object> defMap = new Map<String, Object>();
defMap.put('deficiencyNumber', deficiency.Deficiency_Number__c);
defMap.put('description', deficiency.Description__c);
defMap.put('resolution', deficiency.Resolution__c);
deficiencyList.add(defMap);
}
}
payload.put('DeficiencyList', deficiencyList);
return payload;
}
/**
* @description Returns CLM payload as JSON string for API transmission.
* @param caseId Appraiser_Case__c record Id
* @return String JSON representation of payload
*/
public static String buildPayloadJson(String caseId) {
Map<String, Object> payload = buildPayload(caseId);
return JSON.serialize(payload);
}
/**
* @description Query Appraiser Case with related Deficiencies ordered by number.
* @param caseId Appraiser_Case__c record Id
* @return Appraiser_Case__c Record with Deficiencies__r populated
*/
private static Appraiser_Case__c queryAppraiserCase(String caseId) {
List<Appraiser_Case__c> results = [
SELECT
Id,
Name,
Appraiser_Field_Review_Date__c,
Property_Address__c,
(SELECT Id, Deficiency_Number__c, Description__c, Resolution__c
FROM Deficiencies__r
ORDER BY Deficiency_Number__c ASC)
FROM Appraiser_Case__c
WHERE Id = :caseId
LIMIT 1
];
return results.isEmpty() ? null : results.get(0);
}
/**
* @description Format date for CLM merge (YYYY-MM-DD or null).
* @param dt Date field value
* @return String Formatted date or null
*/
private static String formatDate(Date dt) {
return dt != null ? dt.format() : null;
}
}

View File

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

View File

@ -0,0 +1,80 @@
@IsTest
private class AppraiserCasePayloadBuilderTest {
@TestSetup
static void setupTestData() {
// Create test Appraiser Case
Appraiser_Case__c testCase = new Appraiser_Case__c(
Appraiser_Field_Review_Date__c = Date.parse('04/02/2026'),
Property_Address__c = '123 Main St, Denver, CO 80202'
);
insert testCase;
// Create test deficiency records
List<Appraiser_Case_Deficiency__c> testDefs = new List<Appraiser_Case_Deficiency__c>();
testDefs.add(new Appraiser_Case_Deficiency__c(
Appraiser_Case__c = testCase.Id,
Deficiency_Number__c = 1,
Description__c = 'Missing comparable sale adjustment detail.',
Resolution__c = 'Added adjustment rationale and supporting calculations.'
));
testDefs.add(new Appraiser_Case_Deficiency__c(
Appraiser_Case__c = testCase.Id,
Deficiency_Number__c = 2,
Description__c = 'Neighborhood trend explanation insufficient.',
Resolution__c = 'Expanded market trend narrative with MLS evidence.'
));
insert testDefs;
}
@IsTest
static void testBuildPayload() {
Appraiser_Case__c testCase = [SELECT Id FROM Appraiser_Case__c LIMIT 1];
Map<String, Object> payload = AppraiserCasePayloadBuilder.buildPayload(testCase.Id);
Assert.isNotNull(payload, 'Payload should not be null');
Assert.isTrue(payload.containsKey('AppraiserCaseNumber'), 'Payload should contain AppraiserCaseNumber');
Assert.isTrue(payload.containsKey('AppraiserFieldReviewDate'), 'Payload should contain AppraiserFieldReviewDate');
Assert.isTrue(payload.containsKey('PropertyAddress'), 'Payload should contain PropertyAddress');
Assert.isTrue(payload.containsKey('DeficiencyList'), 'Payload should contain DeficiencyList');
List<Object> deficiencyList = (List<Object>) payload.get('DeficiencyList');
Assert.areEqual(2, deficiencyList.size(), 'DeficiencyList should contain 2 items');
}
@IsTest
static void testBuildPayloadJson() {
Appraiser_Case__c testCase = [SELECT Id FROM Appraiser_Case__c LIMIT 1];
String jsonPayload = AppraiserCasePayloadBuilder.buildPayloadJson(testCase.Id);
Assert.isNotNull(jsonPayload, 'JSON payload should not be null');
Assert.isTrue(jsonPayload.contains('AppraiserCaseNumber'), 'JSON should contain AppraiserCaseNumber');
Assert.isTrue(jsonPayload.contains('DeficiencyList'), 'JSON should contain DeficiencyList');
}
@IsTest
static void testPayloadWithNullDate() {
// Create case without review date
Appraiser_Case__c testCase = new Appraiser_Case__c(
Property_Address__c = '456 Oak Ave, Boulder, CO 80301'
);
insert testCase;
Map<String, Object> payload = AppraiserCasePayloadBuilder.buildPayload(testCase.Id);
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');
}
@IsTest
static void testInvalidCaseId() {
try {
AppraiserCasePayloadBuilder.buildPayload('a0wKW000000000000');
Assert.fail('Should have thrown exception for invalid case id');
} catch (IllegalArgumentException ex) {
Assert.isTrue(ex.getMessage().contains('Appraiser Case not found'));
}
}
}

View File

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

View File

@ -0,0 +1,172 @@
public class CLMDocGenCallout {
// S1 demo environment
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;
// UAT demo environment
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 Integer HTTP_TIMEOUT = 30000;
/** Resolve the correct CLM base URL. env: 'UAT' or 'S1'. */
private static String clmBase(String env) {
return env == 'S1' ? CLM_BASE_S1 : CLM_BASE_UAT;
}
/** Defaults to UAT environment. */
public static CLMDocGenResponse generateDocument(
String caseId,
String templateDocHref,
String destinationFolderHref,
String destinationDocName
) {
return generateDocument(caseId, templateDocHref, destinationFolderHref, destinationDocName, 'UAT');
}
/**
* Generate a merged document via CLM documentxmlmergetasks (no user interaction).
* @param caseId Appraiser_Case__c record Id
* @param templateDocHref Full CLM Href URL of the template .docx document
* @param destinationFolderHref Full CLM Href URL of the destination folder
* @param destinationDocName Filename for the generated document, e.g. "Review_AC-00001.docx"
* @param env 'UAT' (apiuatna11.springcm.com) or 'S1' (api.s1.us.clm.demo.docusign.net)
*/
public static CLMDocGenResponse generateDocument(
String caseId,
String templateDocHref,
String destinationFolderHref,
String destinationDocName,
String env
) {
Map<String, Object> casePayload = AppraiserCasePayloadBuilder.buildPayload(caseId);
Map<String, Object> requestBody = new Map<String, Object>{
'TemplateDocument' => new Map<String, Object>{
'Href' => templateDocHref
},
'DataXML' => buildDataXml(casePayload),
'DestinationDocumentName' => destinationDocName,
'DestinationFolder' => new Map<String, Object>{
'Href' => destinationFolderHref
}
};
HttpRequest req = new HttpRequest();
req.setEndpoint(clmBase(env) + '/documentxmlmergetasks');
req.setMethod('POST');
req.setHeader('Content-Type', 'application/json');
req.setTimeout(HTTP_TIMEOUT);
req.setBody(JSON.serialize(requestBody));
try {
Http http = new Http();
HttpResponse res = http.send(req);
return parseTaskResponse(res);
} catch (Exception ex) {
return new CLMDocGenResponse(false, 'HTTP Callout Error: ' + ex.getMessage(), null, null);
}
}
/** Poll the status of a submitted merge task by its GUID. */
/** Poll the status of a submitted merge task by its GUID (defaults to UAT). */
public static CLMDocGenResponse getTaskStatus(String taskId) {
return getTaskStatus(taskId, 'UAT');
}
public static CLMDocGenResponse getTaskStatus(String taskId, String env) {
HttpRequest req = new HttpRequest();
req.setEndpoint(clmBase(env) + '/documentxmlmergetasks/' + taskId);
req.setMethod('GET');
req.setTimeout(HTTP_TIMEOUT);
try {
Http http = new Http();
HttpResponse res = http.send(req);
return parseTaskResponse(res);
} catch (Exception ex) {
return new CLMDocGenResponse(false, 'HTTP Callout Error: ' + ex.getMessage(), null, null);
}
}
/** Probe any CLM resource path for debugging. env: 'UAT' or 'S1'. */
public static String probe(String resource) {
return probe(resource, 'UAT');
}
public static String probe(String resource, String env) {
HttpRequest req = new HttpRequest();
req.setEndpoint(clmBase(env) + '/' + resource);
req.setMethod('GET');
req.setTimeout(HTTP_TIMEOUT);
HttpResponse res = new Http().send(req);
return 'HTTP ' + res.getStatusCode() + ': ' + res.getBody();
}
/**
* Build the DataXML string from the case payload.
* Flat fields become direct child elements of <TemplateFieldData>.
* DeficiencyList items expand into numbered elements:
* Deficiency_1_Number, Deficiency_1_Description, Deficiency_1_Resolution, etc.
*/
private static String buildDataXml(Map<String, Object> payload) {
String xml = '<TemplateFieldData>';
for (String key : payload.keySet()) {
if (key == 'DeficiencyList') continue;
xml += '<' + key + '>' + escapeXml(String.valueOf(payload.get(key))) + '</' + key + '>';
}
List<Object> deficiencies = (List<Object>) payload.get('DeficiencyList');
if (deficiencies != null) {
for (Integer i = 0; i < deficiencies.size(); i++) {
Map<String, Object> d = (Map<String, Object>) deficiencies[i];
String p = 'Deficiency_' + (i + 1) + '_';
xml += '<' + p + 'Number>' + escapeXml(String.valueOf(d.get('deficiencyNumber'))) + '</' + p + 'Number>';
xml += '<' + p + 'Description>' + escapeXml(String.valueOf(d.get('description'))) + '</' + p + 'Description>';
xml += '<' + p + 'Resolution>' + escapeXml(String.valueOf(d.get('resolution'))) + '</' + p + 'Resolution>';
}
xml += '<DeficiencyCount>' + deficiencies.size() + '</DeficiencyCount>';
}
xml += '</TemplateFieldData>';
return xml;
}
private static String escapeXml(String s) {
if (s == null) return '';
return s.replace('&', '&amp;')
.replace('<', '&lt;')
.replace('>', '&gt;')
.replace('"', '&quot;')
.replace('\'', '&apos;');
}
private static CLMDocGenResponse parseTaskResponse(HttpResponse res) {
Integer statusCode = res.getStatusCode();
String body = res.getBody();
if (statusCode >= 200 && statusCode < 300) {
Map<String, Object> m = (Map<String, Object>) JSON.deserializeUntyped(body);
String href = (String) m.get('Href');
String status = (String) m.get('Status');
String taskId = href != null ? href.substringAfterLast('/') : null;
return new CLMDocGenResponse(true, 'Task status: ' + status, href, taskId);
} else {
return new CLMDocGenResponse(false, 'CLM API Error (HTTP ' + statusCode + '): ' + body, null, null);
}
}
public class CLMDocGenResponse {
public Boolean success;
public String message;
public String documentUrl;
public String documentId;
public CLMDocGenResponse(Boolean success, String message, String documentUrl, String documentId) {
this.success = success;
this.message = message;
this.documentUrl = documentUrl;
this.documentId = documentId;
}
}
}

View File

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

View File

@ -0,0 +1,98 @@
<?xml version="1.0" encoding="UTF-8"?>
<ExternalCredential xmlns="http://soap.sforce.com/2006/04/metadata">
<authenticationProtocol>Oauth</authenticationProtocol>
<externalCredentialParameters>
<certificate>DocusignJWT</certificate>
<parameterGroup>DefaultGroup</parameterGroup>
<parameterName>SigningCertificate</parameterName>
<parameterType>SigningCertificate</parameterType>
</externalCredentialParameters>
<externalCredentialParameters>
<description>Issuer</description>
<parameterGroup>DefaultGroup</parameterGroup>
<parameterName>iss</parameterName>
<parameterType>JwtBodyClaim</parameterType>
<parameterValue>fb613701-2c6c-44a9-9e05-3a0c17e9e3d3</parameterValue>
</externalCredentialParameters>
<externalCredentialParameters>
<description>Subject</description>
<parameterGroup>DefaultGroup</parameterGroup>
<parameterName>sub</parameterName>
<parameterType>JwtBodyClaim</parameterType>
<parameterValue>d9aab149-ff54-408c-a748-baa4b56e2fcd</parameterValue>
</externalCredentialParameters>
<externalCredentialParameters>
<description>Audience</description>
<parameterGroup>DefaultGroup</parameterGroup>
<parameterName>aud</parameterName>
<parameterType>JwtBodyClaim</parameterType>
<parameterValue>account-d.docusign.com</parameterValue>
</externalCredentialParameters>
<externalCredentialParameters>
<description>Expiration Time</description>
<parameterGroup>DefaultGroup</parameterGroup>
<parameterName>exp</parameterName>
<parameterType>JwtBodyClaim</parameterType>
<parameterValue>{!Text(FLOOR((NOW() - DATETIMEVALUE( &quot;1970-01-01 00:00:00&quot; )) * 86400 + 3600))}</parameterValue>
</externalCredentialParameters>
<externalCredentialParameters>
<description>Algorithm</description>
<parameterGroup>DefaultGroup</parameterGroup>
<parameterName>alg</parameterName>
<parameterType>JwtHeaderClaim</parameterType>
<parameterValue>RS256</parameterValue>
</externalCredentialParameters>
<externalCredentialParameters>
<description>Type</description>
<parameterGroup>DefaultGroup</parameterGroup>
<parameterName>typ</parameterName>
<parameterType>JwtHeaderClaim</parameterType>
<parameterValue>JWT</parameterValue>
</externalCredentialParameters>
<externalCredentialParameters>
<description>Issued At</description>
<parameterGroup>DefaultGroup</parameterGroup>
<parameterName>iat</parameterName>
<parameterType>JwtBodyClaim</parameterType>
<parameterValue>{!Text(FLOOR((NOW() - DATETIMEVALUE( &quot;1970-01-01 00:00:00&quot; )) * 86400))}</parameterValue>
</externalCredentialParameters>
<externalCredentialParameters>
<description>Not Before</description>
<parameterGroup>DefaultGroup</parameterGroup>
<parameterName>nbf</parameterName>
<parameterType>JwtBodyClaim</parameterType>
<parameterValue>{!Text(FLOOR((NOW() - DATETIMEVALUE( &quot;1970-01-01 00:00:00&quot; )) * 86400))}</parameterValue>
</externalCredentialParameters>
<externalCredentialParameters>
<description>Key ID</description>
<parameterGroup>DefaultGroup</parameterGroup>
<parameterName>kid</parameterName>
<parameterType>JwtHeaderClaim</parameterType>
<parameterValue>DocusignJWT</parameterValue>
</externalCredentialParameters>
<externalCredentialParameters>
<parameterGroup>DefaultGroup</parameterGroup>
<parameterName>Oauth</parameterName>
<parameterType>AuthProtocolVariant</parameterType>
<parameterValue>JwtBearer</parameterValue>
</externalCredentialParameters>
<externalCredentialParameters>
<description>Scope</description>
<parameterGroup>DefaultGroup</parameterGroup>
<parameterName>scope</parameterName>
<parameterType>JwtBodyClaim</parameterType>
<parameterValue>signature impersonation spring_read spring_write</parameterValue>
</externalCredentialParameters>
<externalCredentialParameters>
<parameterGroup>DefaultGroup</parameterGroup>
<parameterName>AuthProviderUrl</parameterName>
<parameterType>AuthProviderUrl</parameterType>
<parameterValue>https://account-d.docusign.com/oauth/token</parameterValue>
</externalCredentialParameters>
<externalCredentialParameters>
<parameterName>DefaultGroup</parameterName>
<parameterType>NamedPrincipal</parameterType>
<sequenceNumber>1</sequenceNumber>
</externalCredentialParameters>
<label>DocusignJWT</label>
</ExternalCredential>

View File

@ -0,0 +1,24 @@
<?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>CLMNamedCred</label>
<namedCredentialParameters>
<parameterName>Url</parameterName>
<parameterType>Url</parameterType>
<parameterValue>https://api.s1.us.clm.demo.docusign.net</parameterValue>
</namedCredentialParameters>
<namedCredentialParameters>
<externalCredential>DocusignJWT</externalCredential>
<parameterName>ExternalCredential</parameterName>
<parameterType>Authentication</parameterType>
</namedCredentialParameters>
<namedCredentialParameters>
<certificate>DocusignJWT</certificate>
<parameterName>ClientCertificate</parameterName>
<parameterType>ClientCertificate</parameterType>
</namedCredentialParameters>
<namedCredentialType>SecuredEndpoint</namedCredentialType>
</NamedCredential>

View File

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

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<CustomObject xmlns="http://soap.sforce.com/2006/04/metadata">
<deploymentStatus>Deployed</deploymentStatus>
<description>Repeatable deficiency rows for each appraiser case.</description>
<enableActivities>true</enableActivities>
<enableReports>true</enableReports>
<enableSearch>true</enableSearch>
<label>Appraiser Case Deficiency</label>
<nameField>
<displayFormat>DEF-{00000}</displayFormat>
<label>Deficiency Record Number</label>
<type>AutoNumber</type>
</nameField>
<pluralLabel>Appraiser Case Deficiencies</pluralLabel>
<sharingModel>ControlledByParent</sharingModel>
</CustomObject>

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>Appraiser_Case__c</fullName>
<description>Parent appraiser case for this deficiency row.</description>
<label>Appraiser Case</label>
<referenceTo>Appraiser_Case__c</referenceTo>
<relationshipLabel>Deficiencies</relationshipLabel>
<relationshipName>Deficiencies</relationshipName>
<relationshipOrder>0</relationshipOrder>
<reparentableMasterDetail>false</reparentableMasterDetail>
<trackHistory>false</trackHistory>
<type>MasterDetail</type>
<writeRequiresMasterRead>false</writeRequiresMasterRead>
</CustomField>

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>Deficiency_Number__c</fullName>
<description>Business sequence number for a deficiency in the letter.</description>
<label>Deficiency Number</label>
<precision>6</precision>
<required>false</required>
<scale>0</scale>
<trackHistory>false</trackHistory>
<type>Number</type>
</CustomField>

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>Description__c</fullName>
<description>Deficiency description provided by the reviewer.</description>
<label>Description</label>
<length>32768</length>
<required>false</required>
<trackHistory>false</trackHistory>
<type>LongTextArea</type>
<visibleLines>3</visibleLines>
</CustomField>

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>Resolution__c</fullName>
<description>Resolution text for the deficiency item.</description>
<label>Resolution</label>
<length>32768</length>
<required>false</required>
<trackHistory>false</trackHistory>
<type>LongTextArea</type>
<visibleLines>3</visibleLines>
</CustomField>

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<CustomObject xmlns="http://soap.sforce.com/2006/04/metadata">
<deploymentStatus>Deployed</deploymentStatus>
<description>Main record for appraiser review letter generation in CLM.</description>
<enableActivities>true</enableActivities>
<enableReports>true</enableReports>
<enableSearch>true</enableSearch>
<label>Appraiser Case</label>
<nameField>
<displayFormat>AC-{00000}</displayFormat>
<label>Appraiser Case Number</label>
<type>AutoNumber</type>
</nameField>
<pluralLabel>Appraiser Cases</pluralLabel>
<sharingModel>ReadWrite</sharingModel>
</CustomObject>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>Appraiser_Field_Review_Date__c</fullName>
<description>Date the appraiser field review was completed.</description>
<label>Appraiser Field Review Date</label>
<required>false</required>
<trackHistory>false</trackHistory>
<type>Date</type>
</CustomField>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>Property_Address__c</fullName>
<description>Subject property address used in generated review letter.</description>
<label>Property Address</label>
<length>255</length>
<required>false</required>
<trackHistory>false</trackHistory>
<type>Text</type>
</CustomField>

View File

@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8"?>
<PermissionSet xmlns="http://soap.sforce.com/2006/04/metadata">
<description>Access to Appraiser Case records and deficiency rows for CLM generation.</description>
<fieldPermissions>
<editable>true</editable>
<field>Appraiser_Case__c.Appraiser_Field_Review_Date__c</field>
<readable>true</readable>
</fieldPermissions>
<fieldPermissions>
<editable>true</editable>
<field>Appraiser_Case__c.Property_Address__c</field>
<readable>true</readable>
</fieldPermissions>
<fieldPermissions>
<editable>true</editable>
<field>Appraiser_Case_Deficiency__c.Deficiency_Number__c</field>
<readable>true</readable>
</fieldPermissions>
<fieldPermissions>
<editable>true</editable>
<field>Appraiser_Case_Deficiency__c.Description__c</field>
<readable>true</readable>
</fieldPermissions>
<fieldPermissions>
<editable>true</editable>
<field>Appraiser_Case_Deficiency__c.Resolution__c</field>
<readable>true</readable>
</fieldPermissions>
<externalCredentialPrincipalAccesses>
<enabled>true</enabled>
<externalCredentialPrincipal>DocusignJWT-DefaultGroup</externalCredentialPrincipal>
</externalCredentialPrincipalAccesses>
<hasActivationRequired>false</hasActivationRequired>
<label>Appraiser Case Access</label>
<objectPermissions>
<allowCreate>true</allowCreate>
<allowDelete>true</allowDelete>
<allowEdit>true</allowEdit>
<allowRead>true</allowRead>
<modifyAllRecords>false</modifyAllRecords>
<object>Appraiser_Case__c</object>
<viewAllRecords>false</viewAllRecords>
</objectPermissions>
<objectPermissions>
<allowCreate>true</allowCreate>
<allowDelete>true</allowDelete>
<allowEdit>true</allowEdit>
<allowRead>true</allowRead>
<modifyAllRecords>false</modifyAllRecords>
<object>Appraiser_Case_Deficiency__c</object>
<viewAllRecords>false</viewAllRecords>
</objectPermissions>
</PermissionSet>

23
manifest/package.xml Normal file
View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<Package xmlns="http://soap.sforce.com/2006/04/metadata">
<types>
<members>Appraiser_Case__c</members>
<members>Appraiser_Case_Deficiency__c</members>
<name>CustomObject</name>
</types>
<types>
<members>Appraiser_Case_Access</members>
<name>PermissionSet</name>
</types>
<types>
<members>AppraiserCasePayloadBuilder</members>
<members>AppraiserCasePayloadBuilderTest</members>
<members>CLMDocGenCallout</members>
<name>ApexClass</name>
</types>
<types>
<members>DocusignJWT</members>
<name>ExternalCredential</name>
</types>
<version>62.0</version>
</Package>

12
sfdx-project.json Normal file
View File

@ -0,0 +1,12 @@
{
"packageDirectories": [
{
"path": "force-app",
"default": true
}
],
"name": "salesforce-appraiser-review-letter",
"namespace": "",
"sfdcLoginUrl": "https://login.salesforce.com",
"sourceApiVersion": "62.0"
}