commit 63b1bfd7589081cddb5cf30a043941b1f21a99f7 Author: paulh Date: Fri Apr 3 12:13:59 2026 -0400 Initial commit: Salesforce Appraiser Review Letter with DocuSign CLM integration 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 0000000..b64c79f Binary files /dev/null and b/docs/AppraiserReviewLetter_Template.docx differ 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