Initial commit: Salesforce Appraiser Review Letter with DocuSign CLM integration
This commit is contained in:
commit
63b1bfd758
|
|
@ -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.
|
|
@ -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._
|
||||
|
|
@ -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._
|
||||
|
|
@ -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._
|
||||
|
|
@ -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._
|
||||
|
|
@ -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._
|
||||
|
|
@ -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._
|
||||
|
|
@ -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.**
|
||||
|
|
@ -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._
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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('&', '&')
|
||||
.replace('<', '<')
|
||||
.replace('>', '>')
|
||||
.replace('"', '"')
|
||||
.replace('\'', ''');
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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( "1970-01-01 00:00:00" )) * 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( "1970-01-01 00:00:00" )) * 86400))}</parameterValue>
|
||||
</externalCredentialParameters>
|
||||
<externalCredentialParameters>
|
||||
<description>Not Before</description>
|
||||
<parameterGroup>DefaultGroup</parameterGroup>
|
||||
<parameterName>nbf</parameterName>
|
||||
<parameterType>JwtBodyClaim</parameterType>
|
||||
<parameterValue>{!Text(FLOOR((NOW() - DATETIMEVALUE( "1970-01-01 00:00:00" )) * 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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"packageDirectories": [
|
||||
{
|
||||
"path": "force-app",
|
||||
"default": true
|
||||
}
|
||||
],
|
||||
"name": "salesforce-appraiser-review-letter",
|
||||
"namespace": "",
|
||||
"sfdcLoginUrl": "https://login.salesforce.com",
|
||||
"sourceApiVersion": "62.0"
|
||||
}
|
||||
Loading…
Reference in New Issue