From 63b1bfd7589081cddb5cf30a043941b1f21a99f7 Mon Sep 17 00:00:00 2001 From: paulh Date: Fri, 3 Apr 2026 12:13:59 -0400 Subject: [PATCH] Initial commit: Salesforce Appraiser Review Letter with DocuSign CLM integration --- .gitignore | 17 + docs/AppraiserReviewLetter_Template.docx | Bin 0 -> 37243 bytes docs/CLM_INTEGRATION.md | 323 ++++++++++++++++++ docs/CLM_TEMPLATE_GUIDE.md | 89 +++++ docs/DEPLOYMENT_AND_TESTING.md | 82 +++++ docs/FEATURES_UPDATE.md | 19 ++ docs/README.md | 28 ++ docs/design.md | 45 +++ docs/document-plan.md | 30 ++ docs/requirements.md | 80 +++++ .../classes/AppraiserCasePayloadBuilder.cls | 83 +++++ .../AppraiserCasePayloadBuilder.cls-meta.xml | 5 + .../AppraiserCasePayloadBuilderTest.cls | 80 +++++ ...praiserCasePayloadBuilderTest.cls-meta.xml | 5 + .../main/default/classes/CLMDocGenCallout.cls | 172 ++++++++++ .../classes/CLMDocGenCallout.cls-meta.xml | 5 + .../DocusignJWT.externalCredential-meta.xml | 98 ++++++ .../CLMNamedCred.namedCredential-meta.xml | 24 ++ .../CLMuatNamedCreds.namedCredential-meta.xml | 19 ++ ...praiser_Case_Deficiency__c.object-meta.xml | 16 + .../fields/Appraiser_Case__c.field-meta.xml | 14 + .../Deficiency_Number__c.field-meta.xml | 11 + .../fields/Description__c.field-meta.xml | 11 + .../fields/Resolution__c.field-meta.xml | 11 + .../Appraiser_Case__c.object-meta.xml | 16 + ...raiser_Field_Review_Date__c.field-meta.xml | 9 + .../fields/Property_Address__c.field-meta.xml | 10 + ...praiser_Case_Access.permissionset-meta.xml | 53 +++ manifest/package.xml | 23 ++ sfdx-project.json | 12 + 30 files changed, 1390 insertions(+) create mode 100644 .gitignore create mode 100644 docs/AppraiserReviewLetter_Template.docx create mode 100644 docs/CLM_INTEGRATION.md create mode 100644 docs/CLM_TEMPLATE_GUIDE.md create mode 100644 docs/DEPLOYMENT_AND_TESTING.md create mode 100644 docs/FEATURES_UPDATE.md create mode 100644 docs/README.md create mode 100644 docs/design.md create mode 100644 docs/document-plan.md create mode 100644 docs/requirements.md create mode 100644 force-app/main/default/classes/AppraiserCasePayloadBuilder.cls create mode 100644 force-app/main/default/classes/AppraiserCasePayloadBuilder.cls-meta.xml create mode 100644 force-app/main/default/classes/AppraiserCasePayloadBuilderTest.cls create mode 100644 force-app/main/default/classes/AppraiserCasePayloadBuilderTest.cls-meta.xml create mode 100644 force-app/main/default/classes/CLMDocGenCallout.cls create mode 100644 force-app/main/default/classes/CLMDocGenCallout.cls-meta.xml create mode 100644 force-app/main/default/externalCredentials/DocusignJWT.externalCredential-meta.xml create mode 100644 force-app/main/default/namedCredentials/CLMNamedCred.namedCredential-meta.xml create mode 100644 force-app/main/default/namedCredentials/CLMuatNamedCreds.namedCredential-meta.xml create mode 100644 force-app/main/default/objects/Appraiser_Case_Deficiency__c/Appraiser_Case_Deficiency__c.object-meta.xml create mode 100644 force-app/main/default/objects/Appraiser_Case_Deficiency__c/fields/Appraiser_Case__c.field-meta.xml create mode 100644 force-app/main/default/objects/Appraiser_Case_Deficiency__c/fields/Deficiency_Number__c.field-meta.xml create mode 100644 force-app/main/default/objects/Appraiser_Case_Deficiency__c/fields/Description__c.field-meta.xml create mode 100644 force-app/main/default/objects/Appraiser_Case_Deficiency__c/fields/Resolution__c.field-meta.xml create mode 100644 force-app/main/default/objects/Appraiser_Case__c/Appraiser_Case__c.object-meta.xml create mode 100644 force-app/main/default/objects/Appraiser_Case__c/fields/Appraiser_Field_Review_Date__c.field-meta.xml create mode 100644 force-app/main/default/objects/Appraiser_Case__c/fields/Property_Address__c.field-meta.xml create mode 100644 force-app/main/default/permissionsets/Appraiser_Case_Access.permissionset-meta.xml create mode 100644 manifest/package.xml create mode 100644 sfdx-project.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c564b41 --- /dev/null +++ b/.gitignore @@ -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 diff --git a/docs/AppraiserReviewLetter_Template.docx b/docs/AppraiserReviewLetter_Template.docx new file mode 100644 index 0000000000000000000000000000000000000000..b64c79fff0bd91fc5d9f19257677a9e93dc0355e GIT binary patch literal 37243 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 KkR)%47Yc+Gch#pfN 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 BcVGYj literal 0 HcmV?d00001 diff --git a/docs/CLM_INTEGRATION.md b/docs/CLM_INTEGRATION.md new file mode 100644 index 0000000..cdfcba0 --- /dev/null +++ b/docs/CLM_INTEGRATION.md @@ -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 +

Case Number: {{AppraiserCaseNumber}}

+

Review Date: {{AppraiserFieldReviewDate}}

+

Property: {{PropertyAddress}}

+``` + +**Deficiency repeat block**: +```handlebars + + + + + + + {{#each DeficiencyList}} + + + + + + {{/each}} +
Deficiency #DescriptionResolution
{{deficiencyNumber}}{{description}}{{resolution}}
+``` + +**Conditional (if no deficiencies)**: +```handlebars +{{#if DeficiencyList.length}} + +{{else}} +

No deficiencies found.

+{{/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 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._ diff --git a/docs/CLM_TEMPLATE_GUIDE.md b/docs/CLM_TEMPLATE_GUIDE.md new file mode 100644 index 0000000..eaed9ff --- /dev/null +++ b/docs/CLM_TEMPLATE_GUIDE.md @@ -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}} + + {{DeficiencyType}} + {{DeficiencyDescription}} + +{{/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 + + + + + + + {{#each DeficiencyList}} + + {{/each}} + +
TypeDescription
{{DeficiencyType}}{{DeficiencyDescription}}
+``` + +**Paragraph Blocks:** +```handlebars +{{#each Comments}} +

{{CommentText}}

+{{/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._ diff --git a/docs/DEPLOYMENT_AND_TESTING.md b/docs/DEPLOYMENT_AND_TESTING.md new file mode 100644 index 0000000..0a47b8c --- /dev/null +++ b/docs/DEPLOYMENT_AND_TESTING.md @@ -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 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._ diff --git a/docs/FEATURES_UPDATE.md b/docs/FEATURES_UPDATE.md new file mode 100644 index 0000000..701f46c --- /dev/null +++ b/docs/FEATURES_UPDATE.md @@ -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._ diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..3174650 --- /dev/null +++ b/docs/README.md @@ -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._ diff --git a/docs/design.md b/docs/design.md new file mode 100644 index 0000000..1ba0d10 --- /dev/null +++ b/docs/design.md @@ -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}} +{{DeficiencyType}}{{DeficiencyDescription}} +{{/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._ diff --git a/docs/document-plan.md b/docs/document-plan.md new file mode 100644 index 0000000..de746fd --- /dev/null +++ b/docs/document-plan.md @@ -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.** diff --git a/docs/requirements.md b/docs/requirements.md new file mode 100644 index 0000000..b14821d --- /dev/null +++ b/docs/requirements.md @@ -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._ diff --git a/force-app/main/default/classes/AppraiserCasePayloadBuilder.cls b/force-app/main/default/classes/AppraiserCasePayloadBuilder.cls new file mode 100644 index 0000000..d98e6b7 --- /dev/null +++ b/force-app/main/default/classes/AppraiserCasePayloadBuilder.cls @@ -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 CLM merge data ready for template rendering + */ + public static Map 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 payload = new Map(); + 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> deficiencyList = new List>(); + if (appraiserCase.Deficiencies__r != null && !appraiserCase.Deficiencies__r.isEmpty()) { + for (Appraiser_Case_Deficiency__c deficiency : appraiserCase.Deficiencies__r) { + Map defMap = new Map(); + 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 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 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; + } +} diff --git a/force-app/main/default/classes/AppraiserCasePayloadBuilder.cls-meta.xml b/force-app/main/default/classes/AppraiserCasePayloadBuilder.cls-meta.xml new file mode 100644 index 0000000..998805a --- /dev/null +++ b/force-app/main/default/classes/AppraiserCasePayloadBuilder.cls-meta.xml @@ -0,0 +1,5 @@ + + + 62.0 + Active + diff --git a/force-app/main/default/classes/AppraiserCasePayloadBuilderTest.cls b/force-app/main/default/classes/AppraiserCasePayloadBuilderTest.cls new file mode 100644 index 0000000..99cac08 --- /dev/null +++ b/force-app/main/default/classes/AppraiserCasePayloadBuilderTest.cls @@ -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 testDefs = new List(); + 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 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 deficiencyList = (List) 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 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')); + } + } +} diff --git a/force-app/main/default/classes/AppraiserCasePayloadBuilderTest.cls-meta.xml b/force-app/main/default/classes/AppraiserCasePayloadBuilderTest.cls-meta.xml new file mode 100644 index 0000000..998805a --- /dev/null +++ b/force-app/main/default/classes/AppraiserCasePayloadBuilderTest.cls-meta.xml @@ -0,0 +1,5 @@ + + + 62.0 + Active + diff --git a/force-app/main/default/classes/CLMDocGenCallout.cls b/force-app/main/default/classes/CLMDocGenCallout.cls new file mode 100644 index 0000000..d281801 --- /dev/null +++ b/force-app/main/default/classes/CLMDocGenCallout.cls @@ -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 casePayload = AppraiserCasePayloadBuilder.buildPayload(caseId); + + Map requestBody = new Map{ + 'TemplateDocument' => new Map{ + 'Href' => templateDocHref + }, + 'DataXML' => buildDataXml(casePayload), + 'DestinationDocumentName' => destinationDocName, + 'DestinationFolder' => new Map{ + '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 . + * DeficiencyList items expand into numbered elements: + * Deficiency_1_Number, Deficiency_1_Description, Deficiency_1_Resolution, etc. + */ + private static String buildDataXml(Map payload) { + String xml = ''; + + for (String key : payload.keySet()) { + if (key == 'DeficiencyList') continue; + xml += '<' + key + '>' + escapeXml(String.valueOf(payload.get(key))) + ''; + } + + List deficiencies = (List) payload.get('DeficiencyList'); + if (deficiencies != null) { + for (Integer i = 0; i < deficiencies.size(); i++) { + Map d = (Map) deficiencies[i]; + String p = 'Deficiency_' + (i + 1) + '_'; + xml += '<' + p + 'Number>' + escapeXml(String.valueOf(d.get('deficiencyNumber'))) + ''; + xml += '<' + p + 'Description>' + escapeXml(String.valueOf(d.get('description'))) + ''; + xml += '<' + p + 'Resolution>' + escapeXml(String.valueOf(d.get('resolution'))) + ''; + } + xml += '' + deficiencies.size() + ''; + } + + xml += ''; + 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 m = (Map) 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; + } + } +} diff --git a/force-app/main/default/classes/CLMDocGenCallout.cls-meta.xml b/force-app/main/default/classes/CLMDocGenCallout.cls-meta.xml new file mode 100644 index 0000000..998805a --- /dev/null +++ b/force-app/main/default/classes/CLMDocGenCallout.cls-meta.xml @@ -0,0 +1,5 @@ + + + 62.0 + Active + diff --git a/force-app/main/default/externalCredentials/DocusignJWT.externalCredential-meta.xml b/force-app/main/default/externalCredentials/DocusignJWT.externalCredential-meta.xml new file mode 100644 index 0000000..97f7d42 --- /dev/null +++ b/force-app/main/default/externalCredentials/DocusignJWT.externalCredential-meta.xml @@ -0,0 +1,98 @@ + + + Oauth + + DocusignJWT + DefaultGroup + SigningCertificate + SigningCertificate + + + Issuer + DefaultGroup + iss + JwtBodyClaim + fb613701-2c6c-44a9-9e05-3a0c17e9e3d3 + + + Subject + DefaultGroup + sub + JwtBodyClaim + d9aab149-ff54-408c-a748-baa4b56e2fcd + + + Audience + DefaultGroup + aud + JwtBodyClaim + account-d.docusign.com + + + Expiration Time + DefaultGroup + exp + JwtBodyClaim + {!Text(FLOOR((NOW() - DATETIMEVALUE( "1970-01-01 00:00:00" )) * 86400 + 3600))} + + + Algorithm + DefaultGroup + alg + JwtHeaderClaim + RS256 + + + Type + DefaultGroup + typ + JwtHeaderClaim + JWT + + + Issued At + DefaultGroup + iat + JwtBodyClaim + {!Text(FLOOR((NOW() - DATETIMEVALUE( "1970-01-01 00:00:00" )) * 86400))} + + + Not Before + DefaultGroup + nbf + JwtBodyClaim + {!Text(FLOOR((NOW() - DATETIMEVALUE( "1970-01-01 00:00:00" )) * 86400))} + + + Key ID + DefaultGroup + kid + JwtHeaderClaim + DocusignJWT + + + DefaultGroup + Oauth + AuthProtocolVariant + JwtBearer + + + Scope + DefaultGroup + scope + JwtBodyClaim + signature impersonation spring_read spring_write + + + DefaultGroup + AuthProviderUrl + AuthProviderUrl + https://account-d.docusign.com/oauth/token + + + DefaultGroup + NamedPrincipal + 1 + + + diff --git a/force-app/main/default/namedCredentials/CLMNamedCred.namedCredential-meta.xml b/force-app/main/default/namedCredentials/CLMNamedCred.namedCredential-meta.xml new file mode 100644 index 0000000..46f1061 --- /dev/null +++ b/force-app/main/default/namedCredentials/CLMNamedCred.namedCredential-meta.xml @@ -0,0 +1,24 @@ + + + false + false + Enabled + true + + + Url + Url + https://api.s1.us.clm.demo.docusign.net + + + DocusignJWT + ExternalCredential + Authentication + + + DocusignJWT + ClientCertificate + ClientCertificate + + SecuredEndpoint + diff --git a/force-app/main/default/namedCredentials/CLMuatNamedCreds.namedCredential-meta.xml b/force-app/main/default/namedCredentials/CLMuatNamedCreds.namedCredential-meta.xml new file mode 100644 index 0000000..55f754c --- /dev/null +++ b/force-app/main/default/namedCredentials/CLMuatNamedCreds.namedCredential-meta.xml @@ -0,0 +1,19 @@ + + + false + false + Enabled + true + + + Url + Url + https://apiuatna11.springcm.com + + + DocusignJWT + ExternalCredential + Authentication + + SecuredEndpoint + diff --git a/force-app/main/default/objects/Appraiser_Case_Deficiency__c/Appraiser_Case_Deficiency__c.object-meta.xml b/force-app/main/default/objects/Appraiser_Case_Deficiency__c/Appraiser_Case_Deficiency__c.object-meta.xml new file mode 100644 index 0000000..092b820 --- /dev/null +++ b/force-app/main/default/objects/Appraiser_Case_Deficiency__c/Appraiser_Case_Deficiency__c.object-meta.xml @@ -0,0 +1,16 @@ + + + Deployed + Repeatable deficiency rows for each appraiser case. + true + true + true + + + DEF-{00000} + + AutoNumber + + Appraiser Case Deficiencies + ControlledByParent + \ No newline at end of file diff --git a/force-app/main/default/objects/Appraiser_Case_Deficiency__c/fields/Appraiser_Case__c.field-meta.xml b/force-app/main/default/objects/Appraiser_Case_Deficiency__c/fields/Appraiser_Case__c.field-meta.xml new file mode 100644 index 0000000..f4292d6 --- /dev/null +++ b/force-app/main/default/objects/Appraiser_Case_Deficiency__c/fields/Appraiser_Case__c.field-meta.xml @@ -0,0 +1,14 @@ + + + Appraiser_Case__c + Parent appraiser case for this deficiency row. + + Appraiser_Case__c + Deficiencies + Deficiencies + 0 + false + false + MasterDetail + false + \ No newline at end of file diff --git a/force-app/main/default/objects/Appraiser_Case_Deficiency__c/fields/Deficiency_Number__c.field-meta.xml b/force-app/main/default/objects/Appraiser_Case_Deficiency__c/fields/Deficiency_Number__c.field-meta.xml new file mode 100644 index 0000000..d56c346 --- /dev/null +++ b/force-app/main/default/objects/Appraiser_Case_Deficiency__c/fields/Deficiency_Number__c.field-meta.xml @@ -0,0 +1,11 @@ + + + Deficiency_Number__c + Business sequence number for a deficiency in the letter. + + 6 + false + 0 + false + Number + \ No newline at end of file diff --git a/force-app/main/default/objects/Appraiser_Case_Deficiency__c/fields/Description__c.field-meta.xml b/force-app/main/default/objects/Appraiser_Case_Deficiency__c/fields/Description__c.field-meta.xml new file mode 100644 index 0000000..5207f72 --- /dev/null +++ b/force-app/main/default/objects/Appraiser_Case_Deficiency__c/fields/Description__c.field-meta.xml @@ -0,0 +1,11 @@ + + + Description__c + Deficiency description provided by the reviewer. + + 32768 + false + false + LongTextArea + 3 + \ No newline at end of file diff --git a/force-app/main/default/objects/Appraiser_Case_Deficiency__c/fields/Resolution__c.field-meta.xml b/force-app/main/default/objects/Appraiser_Case_Deficiency__c/fields/Resolution__c.field-meta.xml new file mode 100644 index 0000000..dc2aeb9 --- /dev/null +++ b/force-app/main/default/objects/Appraiser_Case_Deficiency__c/fields/Resolution__c.field-meta.xml @@ -0,0 +1,11 @@ + + + Resolution__c + Resolution text for the deficiency item. + + 32768 + false + false + LongTextArea + 3 + \ No newline at end of file diff --git a/force-app/main/default/objects/Appraiser_Case__c/Appraiser_Case__c.object-meta.xml b/force-app/main/default/objects/Appraiser_Case__c/Appraiser_Case__c.object-meta.xml new file mode 100644 index 0000000..e3f5201 --- /dev/null +++ b/force-app/main/default/objects/Appraiser_Case__c/Appraiser_Case__c.object-meta.xml @@ -0,0 +1,16 @@ + + + Deployed + Main record for appraiser review letter generation in CLM. + true + true + true + + + AC-{00000} + + AutoNumber + + Appraiser Cases + ReadWrite + \ No newline at end of file diff --git a/force-app/main/default/objects/Appraiser_Case__c/fields/Appraiser_Field_Review_Date__c.field-meta.xml b/force-app/main/default/objects/Appraiser_Case__c/fields/Appraiser_Field_Review_Date__c.field-meta.xml new file mode 100644 index 0000000..828b779 --- /dev/null +++ b/force-app/main/default/objects/Appraiser_Case__c/fields/Appraiser_Field_Review_Date__c.field-meta.xml @@ -0,0 +1,9 @@ + + + Appraiser_Field_Review_Date__c + Date the appraiser field review was completed. + + false + false + Date + \ No newline at end of file diff --git a/force-app/main/default/objects/Appraiser_Case__c/fields/Property_Address__c.field-meta.xml b/force-app/main/default/objects/Appraiser_Case__c/fields/Property_Address__c.field-meta.xml new file mode 100644 index 0000000..dc03aaf --- /dev/null +++ b/force-app/main/default/objects/Appraiser_Case__c/fields/Property_Address__c.field-meta.xml @@ -0,0 +1,10 @@ + + + Property_Address__c + Subject property address used in generated review letter. + + 255 + false + false + Text + \ No newline at end of file diff --git a/force-app/main/default/permissionsets/Appraiser_Case_Access.permissionset-meta.xml b/force-app/main/default/permissionsets/Appraiser_Case_Access.permissionset-meta.xml new file mode 100644 index 0000000..02e74a9 --- /dev/null +++ b/force-app/main/default/permissionsets/Appraiser_Case_Access.permissionset-meta.xml @@ -0,0 +1,53 @@ + + + Access to Appraiser Case records and deficiency rows for CLM generation. + + true + Appraiser_Case__c.Appraiser_Field_Review_Date__c + true + + + true + Appraiser_Case__c.Property_Address__c + true + + + true + Appraiser_Case_Deficiency__c.Deficiency_Number__c + true + + + true + Appraiser_Case_Deficiency__c.Description__c + true + + + true + Appraiser_Case_Deficiency__c.Resolution__c + true + + + true + DocusignJWT-DefaultGroup + + false + + + true + true + true + true + false + Appraiser_Case__c + false + + + true + true + true + true + false + Appraiser_Case_Deficiency__c + false + + \ No newline at end of file diff --git a/manifest/package.xml b/manifest/package.xml new file mode 100644 index 0000000..fce33a3 --- /dev/null +++ b/manifest/package.xml @@ -0,0 +1,23 @@ + + + + Appraiser_Case__c + Appraiser_Case_Deficiency__c + CustomObject + + + Appraiser_Case_Access + PermissionSet + + + AppraiserCasePayloadBuilder + AppraiserCasePayloadBuilderTest + CLMDocGenCallout + ApexClass + + + DocusignJWT + ExternalCredential + + 62.0 + \ No newline at end of file diff --git a/sfdx-project.json b/sfdx-project.json new file mode 100644 index 0000000..c893112 --- /dev/null +++ b/sfdx-project.json @@ -0,0 +1,12 @@ +{ + "packageDirectories": [ + { + "path": "force-app", + "default": true + } + ], + "name": "salesforce-appraiser-review-letter", + "namespace": "", + "sfdcLoginUrl": "https://login.salesforce.com", + "sourceApiVersion": "62.0" +} \ No newline at end of file