Add CLMAdminService, DocusignESignatureService, and fix code review issues
New classes: - CLMAdminService: UI-facing orchestration — account/letter settings from metadata, document generation, task status polling, file attachment, folder browsing; persists all CLM results to Appraiser_Case__c - DocusignESignatureService: eSignature API browsing (accounts, templates, envelopes, login info, user info) - CLMAdminServiceTest, CLMDocGenCalloutTest, DocusignESignatureServiceTest Updated classes (AppraiserCasePayloadBuilder, CLMDocGenCallout): - CLMDocGenCallout: full XML merge callout stack, task status polling, document download, recursive document href discovery, account-based endpoint building; HTTP_TIMEOUT made public - AppraiserCasePayloadBuilder: formatMailingAddress made public so CLMAdminService can reuse it rather than duplicating the logic Code review bug fixes: - Fix null fields emitting literal "null" in generated XML — add safeValue() helper; String.valueOf(null) returns "null" so escapeXml's null guard never fired - Fix unguarded inline SOQL in getCaseContext and getDocGenPreview — throws QueryException for missing records instead of AuraHandledException; now uses list query with isEmpty guard - Remove duplicate formatAddress in CLMAdminService; delegate to AppraiserCasePayloadBuilder.formatMailingAddress - Replace hardcoded 30000 timeout in performGet with CLMDocGenCallout.HTTP_TIMEOUT - Remove duplicate JSDoc on getTaskStatus Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
62b78faf1a
commit
45814dc2d5
|
|
@ -21,7 +21,30 @@ public class AppraiserCasePayloadBuilder {
|
||||||
Map<String, Object> payload = new Map<String, Object>();
|
Map<String, Object> payload = new Map<String, Object>();
|
||||||
payload.put('AppraiserCaseNumber', appraiserCase.Name);
|
payload.put('AppraiserCaseNumber', appraiserCase.Name);
|
||||||
payload.put('AppraiserFieldReviewDate', formatDate(appraiserCase.Appraiser_Field_Review_Date__c));
|
payload.put('AppraiserFieldReviewDate', formatDate(appraiserCase.Appraiser_Field_Review_Date__c));
|
||||||
payload.put('PropertyAddress', appraiserCase.Property_Address__c);
|
payload.put('LetterSentDate', formatDate(appraiserCase.Letter_Sent_Date__c));
|
||||||
|
payload.put('FHACaseNumber', appraiserCase.FHA_Case_Number__c);
|
||||||
|
payload.put('AppraiserName', appraiserCase.Appraiser_Name__c);
|
||||||
|
payload.put('AppraiserSalutation', appraiserCase.Appraiser_Salutation__c);
|
||||||
|
payload.put('AppraiserLastName', appraiserCase.Appraiser_Last_Name__c);
|
||||||
|
payload.put('AppraiserEmail', appraiserCase.Appraiser_Email__c);
|
||||||
|
payload.put('AppraiserStreet', appraiserCase.Appraiser_Street__c);
|
||||||
|
payload.put('AppraiserCity', appraiserCase.Appraiser_City__c);
|
||||||
|
payload.put('AppraiserStateProvince', appraiserCase.Appraiser_State_Province__c);
|
||||||
|
payload.put('AppraiserPostalCode', appraiserCase.Appraiser_Postal_Code__c);
|
||||||
|
payload.put('AppraiserCountry', appraiserCase.Appraiser_Country__c);
|
||||||
|
payload.put('AppraiserAddress', formatMailingAddress(
|
||||||
|
appraiserCase.Appraiser_Street__c,
|
||||||
|
appraiserCase.Appraiser_City__c,
|
||||||
|
appraiserCase.Appraiser_State_Province__c,
|
||||||
|
appraiserCase.Appraiser_Postal_Code__c,
|
||||||
|
appraiserCase.Appraiser_Country__c
|
||||||
|
));
|
||||||
|
payload.put('PropertyStreet', appraiserCase.Property_Street__c);
|
||||||
|
payload.put('PropertyCity', appraiserCase.Property_City__c);
|
||||||
|
payload.put('PropertyStateProvince', appraiserCase.Property_State_Province__c);
|
||||||
|
payload.put('PropertyPostalCode', appraiserCase.Property_Postal_Code__c);
|
||||||
|
payload.put('PropertyCountry', appraiserCase.Property_Country__c);
|
||||||
|
payload.put('PropertyAddress', formatAddress(appraiserCase));
|
||||||
|
|
||||||
// Transform child deficiencies into DeficiencyList array
|
// Transform child deficiencies into DeficiencyList array
|
||||||
List<Map<String, Object>> deficiencyList = new List<Map<String, Object>>();
|
List<Map<String, Object>> deficiencyList = new List<Map<String, Object>>();
|
||||||
|
|
@ -31,6 +54,7 @@ public class AppraiserCasePayloadBuilder {
|
||||||
defMap.put('deficiencyNumber', deficiency.Deficiency_Number__c);
|
defMap.put('deficiencyNumber', deficiency.Deficiency_Number__c);
|
||||||
defMap.put('description', deficiency.Description__c);
|
defMap.put('description', deficiency.Description__c);
|
||||||
defMap.put('resolution', deficiency.Resolution__c);
|
defMap.put('resolution', deficiency.Resolution__c);
|
||||||
|
defMap.put('reference', deficiency.Reference__c);
|
||||||
deficiencyList.add(defMap);
|
deficiencyList.add(defMap);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -60,8 +84,23 @@ public class AppraiserCasePayloadBuilder {
|
||||||
Id,
|
Id,
|
||||||
Name,
|
Name,
|
||||||
Appraiser_Field_Review_Date__c,
|
Appraiser_Field_Review_Date__c,
|
||||||
Property_Address__c,
|
Letter_Sent_Date__c,
|
||||||
(SELECT Id, Deficiency_Number__c, Description__c, Resolution__c
|
FHA_Case_Number__c,
|
||||||
|
Appraiser_Name__c,
|
||||||
|
Appraiser_Salutation__c,
|
||||||
|
Appraiser_Last_Name__c,
|
||||||
|
Appraiser_Email__c,
|
||||||
|
Appraiser_Street__c,
|
||||||
|
Appraiser_City__c,
|
||||||
|
Appraiser_State_Province__c,
|
||||||
|
Appraiser_Postal_Code__c,
|
||||||
|
Appraiser_Country__c,
|
||||||
|
Property_Street__c,
|
||||||
|
Property_City__c,
|
||||||
|
Property_State_Province__c,
|
||||||
|
Property_Postal_Code__c,
|
||||||
|
Property_Country__c,
|
||||||
|
(SELECT Id, Deficiency_Number__c, Description__c, Resolution__c, Reference__c
|
||||||
FROM Deficiencies__r
|
FROM Deficiencies__r
|
||||||
ORDER BY Deficiency_Number__c ASC)
|
ORDER BY Deficiency_Number__c ASC)
|
||||||
FROM Appraiser_Case__c
|
FROM Appraiser_Case__c
|
||||||
|
|
@ -73,11 +112,60 @@ public class AppraiserCasePayloadBuilder {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description Format date for CLM merge (YYYY-MM-DD or null).
|
* @description Format date for CLM merge (for example Apr 09, 2026) or null.
|
||||||
* @param dt Date field value
|
* @param dt Date field value
|
||||||
* @return String Formatted date or null
|
* @return String Formatted date or null
|
||||||
*/
|
*/
|
||||||
private static String formatDate(Date dt) {
|
private static String formatDate(Date dt) {
|
||||||
return dt != null ? dt.format() : null;
|
return dt != null
|
||||||
|
? DateTime.newInstance(dt, Time.newInstance(0, 0, 0, 0)).format('MMM dd, yyyy')
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String formatAddress(Appraiser_Case__c appraiserCase) {
|
||||||
|
return formatMailingAddress(
|
||||||
|
appraiserCase.Property_Street__c,
|
||||||
|
appraiserCase.Property_City__c,
|
||||||
|
appraiserCase.Property_State_Province__c,
|
||||||
|
appraiserCase.Property_Postal_Code__c,
|
||||||
|
appraiserCase.Property_Country__c
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String formatMailingAddress(
|
||||||
|
String street,
|
||||||
|
String city,
|
||||||
|
String stateProvince,
|
||||||
|
String postalCode,
|
||||||
|
String country
|
||||||
|
) {
|
||||||
|
List<String> lines = new List<String>();
|
||||||
|
if (String.isNotBlank(street)) {
|
||||||
|
lines.add(street.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> localityParts = new List<String>();
|
||||||
|
if (String.isNotBlank(city)) {
|
||||||
|
localityParts.add(city.trim());
|
||||||
|
}
|
||||||
|
if (String.isNotBlank(stateProvince)) {
|
||||||
|
localityParts.add(stateProvince.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
String locality = String.join(localityParts, ', ');
|
||||||
|
if (String.isNotBlank(postalCode)) {
|
||||||
|
locality = String.isNotBlank(locality)
|
||||||
|
? locality + ' ' + postalCode.trim()
|
||||||
|
: postalCode.trim();
|
||||||
|
}
|
||||||
|
if (String.isNotBlank(locality)) {
|
||||||
|
lines.add(locality);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (String.isNotBlank(country)) {
|
||||||
|
lines.add(country.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines.isEmpty() ? null : String.join(lines, ', ');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,23 @@ private class AppraiserCasePayloadBuilderTest {
|
||||||
static void setupTestData() {
|
static void setupTestData() {
|
||||||
// Create test Appraiser Case
|
// Create test Appraiser Case
|
||||||
Appraiser_Case__c testCase = new Appraiser_Case__c(
|
Appraiser_Case__c testCase = new Appraiser_Case__c(
|
||||||
Appraiser_Field_Review_Date__c = Date.parse('04/02/2026'),
|
Appraiser_Field_Review_Date__c = Date.newInstance(2026, 4, 2),
|
||||||
Property_Address__c = '123 Main St, Denver, CO 80202'
|
Letter_Sent_Date__c = Date.newInstance(2026, 4, 9),
|
||||||
|
FHA_Case_Number__c = '123-4567890',
|
||||||
|
Appraiser_Name__c = 'Jamie Appraiser',
|
||||||
|
Appraiser_Salutation__c = 'Ms.',
|
||||||
|
Appraiser_Last_Name__c = 'Appraiser',
|
||||||
|
Appraiser_Email__c = 'jamie.appraiser@example.com',
|
||||||
|
Appraiser_Street__c = '245 Lexington Ave',
|
||||||
|
Appraiser_City__c = 'New York',
|
||||||
|
Appraiser_State_Province__c = 'NY',
|
||||||
|
Appraiser_Postal_Code__c = '10016',
|
||||||
|
Appraiser_Country__c = 'USA',
|
||||||
|
Property_Street__c = '123 Main St',
|
||||||
|
Property_City__c = 'Denver',
|
||||||
|
Property_State_Province__c = 'CO',
|
||||||
|
Property_Postal_Code__c = '80202',
|
||||||
|
Property_Country__c = 'USA'
|
||||||
);
|
);
|
||||||
insert testCase;
|
insert testCase;
|
||||||
|
|
||||||
|
|
@ -16,13 +31,15 @@ private class AppraiserCasePayloadBuilderTest {
|
||||||
Appraiser_Case__c = testCase.Id,
|
Appraiser_Case__c = testCase.Id,
|
||||||
Deficiency_Number__c = 1,
|
Deficiency_Number__c = 1,
|
||||||
Description__c = 'Missing comparable sale adjustment detail.',
|
Description__c = 'Missing comparable sale adjustment detail.',
|
||||||
Resolution__c = 'Added adjustment rationale and supporting calculations.'
|
Resolution__c = 'Added adjustment rationale and supporting calculations.',
|
||||||
|
Reference__c = 'VC-1'
|
||||||
));
|
));
|
||||||
testDefs.add(new Appraiser_Case_Deficiency__c(
|
testDefs.add(new Appraiser_Case_Deficiency__c(
|
||||||
Appraiser_Case__c = testCase.Id,
|
Appraiser_Case__c = testCase.Id,
|
||||||
Deficiency_Number__c = 2,
|
Deficiency_Number__c = 2,
|
||||||
Description__c = 'Neighborhood trend explanation insufficient.',
|
Description__c = 'Neighborhood trend explanation insufficient.',
|
||||||
Resolution__c = 'Expanded market trend narrative with MLS evidence.'
|
Resolution__c = 'Expanded market trend narrative with MLS evidence.',
|
||||||
|
Reference__c = 'MC-2'
|
||||||
));
|
));
|
||||||
insert testDefs;
|
insert testDefs;
|
||||||
}
|
}
|
||||||
|
|
@ -36,11 +53,39 @@ private class AppraiserCasePayloadBuilderTest {
|
||||||
Assert.isNotNull(payload, 'Payload should not be null');
|
Assert.isNotNull(payload, 'Payload should not be null');
|
||||||
Assert.isTrue(payload.containsKey('AppraiserCaseNumber'), 'Payload should contain AppraiserCaseNumber');
|
Assert.isTrue(payload.containsKey('AppraiserCaseNumber'), 'Payload should contain AppraiserCaseNumber');
|
||||||
Assert.isTrue(payload.containsKey('AppraiserFieldReviewDate'), 'Payload should contain AppraiserFieldReviewDate');
|
Assert.isTrue(payload.containsKey('AppraiserFieldReviewDate'), 'Payload should contain AppraiserFieldReviewDate');
|
||||||
|
Assert.isTrue(payload.containsKey('LetterSentDate'), 'Payload should contain LetterSentDate');
|
||||||
|
Assert.isTrue(payload.containsKey('FHACaseNumber'), 'Payload should contain FHACaseNumber');
|
||||||
|
Assert.isTrue(payload.containsKey('AppraiserName'), 'Payload should contain AppraiserName');
|
||||||
|
Assert.isTrue(payload.containsKey('AppraiserSalutation'), 'Payload should contain AppraiserSalutation');
|
||||||
|
Assert.isTrue(payload.containsKey('AppraiserLastName'), 'Payload should contain AppraiserLastName');
|
||||||
|
Assert.isTrue(payload.containsKey('AppraiserEmail'), 'Payload should contain AppraiserEmail');
|
||||||
|
Assert.isTrue(payload.containsKey('AppraiserAddress'), 'Payload should contain AppraiserAddress');
|
||||||
Assert.isTrue(payload.containsKey('PropertyAddress'), 'Payload should contain PropertyAddress');
|
Assert.isTrue(payload.containsKey('PropertyAddress'), 'Payload should contain PropertyAddress');
|
||||||
|
Assert.isTrue(payload.containsKey('PropertyStreet'), 'Payload should contain PropertyStreet');
|
||||||
|
Assert.isTrue(payload.containsKey('PropertyCity'), 'Payload should contain PropertyCity');
|
||||||
|
Assert.isTrue(payload.containsKey('PropertyStateProvince'), 'Payload should contain PropertyStateProvince');
|
||||||
|
Assert.isTrue(payload.containsKey('PropertyPostalCode'), 'Payload should contain PropertyPostalCode');
|
||||||
|
Assert.isTrue(payload.containsKey('PropertyCountry'), 'Payload should contain PropertyCountry');
|
||||||
Assert.isTrue(payload.containsKey('DeficiencyList'), 'Payload should contain DeficiencyList');
|
Assert.isTrue(payload.containsKey('DeficiencyList'), 'Payload should contain DeficiencyList');
|
||||||
|
Assert.areEqual('123 Main St', (String) payload.get('PropertyStreet'));
|
||||||
|
Assert.areEqual('Denver', (String) payload.get('PropertyCity'));
|
||||||
|
Assert.areEqual('CO', (String) payload.get('PropertyStateProvince'));
|
||||||
|
Assert.areEqual('80202', (String) payload.get('PropertyPostalCode'));
|
||||||
|
Assert.areEqual('USA', (String) payload.get('PropertyCountry'));
|
||||||
|
Assert.areEqual('123-4567890', (String) payload.get('FHACaseNumber'));
|
||||||
|
Assert.areEqual('Apr 02, 2026', (String) payload.get('AppraiserFieldReviewDate'));
|
||||||
|
Assert.areEqual('Apr 09, 2026', (String) payload.get('LetterSentDate'));
|
||||||
|
Assert.areEqual('Jamie Appraiser', (String) payload.get('AppraiserName'));
|
||||||
|
Assert.areEqual('Ms.', (String) payload.get('AppraiserSalutation'));
|
||||||
|
Assert.areEqual('Appraiser', (String) payload.get('AppraiserLastName'));
|
||||||
|
Assert.areEqual('jamie.appraiser@example.com', (String) payload.get('AppraiserEmail'));
|
||||||
|
Assert.areEqual('245 Lexington Ave, New York, NY 10016, USA', (String) payload.get('AppraiserAddress'));
|
||||||
|
Assert.areEqual('123 Main St, Denver, CO 80202, USA', (String) payload.get('PropertyAddress'));
|
||||||
|
|
||||||
List<Object> deficiencyList = (List<Object>) payload.get('DeficiencyList');
|
List<Object> deficiencyList = (List<Object>) payload.get('DeficiencyList');
|
||||||
Assert.areEqual(2, deficiencyList.size(), 'DeficiencyList should contain 2 items');
|
Assert.areEqual(2, deficiencyList.size(), 'DeficiencyList should contain 2 items');
|
||||||
|
Map<String, Object> firstDeficiency = (Map<String, Object>) deficiencyList[0];
|
||||||
|
Assert.areEqual('VC-1', (String) firstDeficiency.get('reference'));
|
||||||
}
|
}
|
||||||
|
|
||||||
@IsTest
|
@IsTest
|
||||||
|
|
@ -58,7 +103,10 @@ private class AppraiserCasePayloadBuilderTest {
|
||||||
static void testPayloadWithNullDate() {
|
static void testPayloadWithNullDate() {
|
||||||
// Create case without review date
|
// Create case without review date
|
||||||
Appraiser_Case__c testCase = new Appraiser_Case__c(
|
Appraiser_Case__c testCase = new Appraiser_Case__c(
|
||||||
Property_Address__c = '456 Oak Ave, Boulder, CO 80301'
|
Property_Street__c = '456 Oak Ave',
|
||||||
|
Property_City__c = 'Boulder',
|
||||||
|
Property_State_Province__c = 'CO',
|
||||||
|
Property_Postal_Code__c = '80301'
|
||||||
);
|
);
|
||||||
insert testCase;
|
insert testCase;
|
||||||
|
|
||||||
|
|
@ -66,6 +114,7 @@ private class AppraiserCasePayloadBuilderTest {
|
||||||
|
|
||||||
Assert.isNotNull(payload, 'Payload should not be null even with null date');
|
Assert.isNotNull(payload, 'Payload should not be null even with null date');
|
||||||
Assert.isNull(payload.get('AppraiserFieldReviewDate'), 'Null date should map to null in payload');
|
Assert.isNull(payload.get('AppraiserFieldReviewDate'), 'Null date should map to null in payload');
|
||||||
|
Assert.areEqual('456 Oak Ave, Boulder, CO 80301', (String) payload.get('PropertyAddress'));
|
||||||
}
|
}
|
||||||
|
|
||||||
@IsTest
|
@IsTest
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,744 @@
|
||||||
|
public with sharing class CLMAdminService {
|
||||||
|
public class CaseDeficiencyItem {
|
||||||
|
@AuraEnabled public Id recordId;
|
||||||
|
@AuraEnabled public Decimal deficiencyNumber;
|
||||||
|
@AuraEnabled public String description;
|
||||||
|
@AuraEnabled public String resolution;
|
||||||
|
@AuraEnabled public String reference;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CaseContext {
|
||||||
|
@AuraEnabled public Id caseId;
|
||||||
|
@AuraEnabled public String caseNumber;
|
||||||
|
@AuraEnabled public String propertyAddress;
|
||||||
|
@AuraEnabled public String lastDocGenStatus;
|
||||||
|
@AuraEnabled public String lastDocGenMessage;
|
||||||
|
@AuraEnabled public String lastClmAccountCode;
|
||||||
|
@AuraEnabled public String lastDocGenTaskId;
|
||||||
|
@AuraEnabled public String lastDocGenTaskUrl;
|
||||||
|
@AuraEnabled public String generatedDocumentUrl;
|
||||||
|
@AuraEnabled public String generatedDocumentId;
|
||||||
|
@AuraEnabled public String attachedFileContentDocumentId;
|
||||||
|
@AuraEnabled public String attachedFileUrl;
|
||||||
|
@AuraEnabled public Datetime lastDocGenRequestedAt;
|
||||||
|
@AuraEnabled public Datetime lastDocGenCompletedAt;
|
||||||
|
@AuraEnabled public List<CaseDeficiencyItem> deficiencies;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class FileAttachmentResult {
|
||||||
|
@AuraEnabled public Boolean success;
|
||||||
|
@AuraEnabled public String message;
|
||||||
|
@AuraEnabled public String contentDocumentId;
|
||||||
|
@AuraEnabled public String fileUrl;
|
||||||
|
@AuraEnabled public String fileTitle;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class AccountSettings {
|
||||||
|
@AuraEnabled public String accountCode;
|
||||||
|
@AuraEnabled public String accountDisplayName;
|
||||||
|
@AuraEnabled public String environment;
|
||||||
|
@AuraEnabled public String clmAccountId;
|
||||||
|
@AuraEnabled public String clmApiNamedCredential;
|
||||||
|
@AuraEnabled public String clmDownloadNamedCredential;
|
||||||
|
@AuraEnabled public String eSignatureRestNamedCredential;
|
||||||
|
@AuraEnabled public String templateRootFolderHref;
|
||||||
|
@AuraEnabled public String destinationRootFolderHref;
|
||||||
|
@AuraEnabled public String defaultTemplateDocumentHref;
|
||||||
|
@AuraEnabled public String defaultDocumentNamePrefix;
|
||||||
|
@AuraEnabled public Boolean active;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class LetterSettings {
|
||||||
|
@AuraEnabled public String accountCode;
|
||||||
|
@AuraEnabled public String letterCode;
|
||||||
|
@AuraEnabled public String letterDisplayName;
|
||||||
|
@AuraEnabled public String description;
|
||||||
|
@AuraEnabled public Boolean isDefault;
|
||||||
|
@AuraEnabled public Decimal sortOrder;
|
||||||
|
@AuraEnabled public String templateRootFolderHref;
|
||||||
|
@AuraEnabled public String destinationRootFolderHref;
|
||||||
|
@AuraEnabled public String defaultTemplateDocumentHref;
|
||||||
|
@AuraEnabled public String defaultDocumentNamePrefix;
|
||||||
|
@AuraEnabled public Boolean active;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ResourceItem {
|
||||||
|
@AuraEnabled public String name;
|
||||||
|
@AuraEnabled public String href;
|
||||||
|
@AuraEnabled public String type;
|
||||||
|
@AuraEnabled public String parentHref;
|
||||||
|
@AuraEnabled public String rawJson;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class FolderContents {
|
||||||
|
@AuraEnabled public ResourceItem folder;
|
||||||
|
@AuraEnabled public List<ResourceItem> folders;
|
||||||
|
@AuraEnabled public List<ResourceItem> documents;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class DocGenPreview {
|
||||||
|
@AuraEnabled public String accountCode;
|
||||||
|
@AuraEnabled public String accountDisplayName;
|
||||||
|
@AuraEnabled public String letterCode;
|
||||||
|
@AuraEnabled public String letterDisplayName;
|
||||||
|
@AuraEnabled public String mergeTaskEndpointUrl;
|
||||||
|
@AuraEnabled public String templateDocHref;
|
||||||
|
@AuraEnabled public String destinationFolderHref;
|
||||||
|
@AuraEnabled public String destinationDocName;
|
||||||
|
@AuraEnabled public String payloadJson;
|
||||||
|
@AuraEnabled public String dataXml;
|
||||||
|
@AuraEnabled public String requestBodyJson;
|
||||||
|
}
|
||||||
|
|
||||||
|
@AuraEnabled(cacheable=true)
|
||||||
|
public static List<AccountSettings> listAccountSettings() {
|
||||||
|
List<AccountSettings> settings = new List<AccountSettings>();
|
||||||
|
for (CLM_Account_Setting__mdt row : [
|
||||||
|
SELECT DeveloperName,
|
||||||
|
Account_Code__c,
|
||||||
|
Account_Display_Name__c,
|
||||||
|
Environment_Code__c,
|
||||||
|
CLM_Account_Id__c,
|
||||||
|
CLM_Api_Named_Credential__c,
|
||||||
|
CLM_Download_Named_Credential__c,
|
||||||
|
ESignature_Rest_Named_Credential__c,
|
||||||
|
Template_Root_Folder_Href__c,
|
||||||
|
Destination_Root_Folder_Href__c,
|
||||||
|
Default_Template_Document_Href__c,
|
||||||
|
Default_Destination_Document_Name_Prefix__c,
|
||||||
|
Active__c
|
||||||
|
FROM CLM_Account_Setting__mdt
|
||||||
|
WHERE Active__c = true
|
||||||
|
ORDER BY Account_Display_Name__c ASC, DeveloperName ASC
|
||||||
|
]) {
|
||||||
|
settings.add(toAccountSettings(row));
|
||||||
|
}
|
||||||
|
return settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
@AuraEnabled(cacheable=true)
|
||||||
|
public static AccountSettings getAccountSettings(String accountCode) {
|
||||||
|
CLM_Account_Setting__mdt row = resolveAccountSetting(accountCode);
|
||||||
|
return row == null ? null : toAccountSettings(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
@AuraEnabled(cacheable=true)
|
||||||
|
public static List<LetterSettings> listLetterSettings(String accountCode) {
|
||||||
|
AccountSettings account = getAccountSettings(accountCode);
|
||||||
|
List<LetterSettings> letters = new List<LetterSettings>();
|
||||||
|
if (account == null) {
|
||||||
|
return letters;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (CLM_Letter_Definition__mdt row : [
|
||||||
|
SELECT DeveloperName,
|
||||||
|
Account_Code__c,
|
||||||
|
Letter_Code__c,
|
||||||
|
Letter_Display_Name__c,
|
||||||
|
Description__c,
|
||||||
|
Active__c,
|
||||||
|
Is_Default__c,
|
||||||
|
Sort_Order__c,
|
||||||
|
Template_Root_Folder_Href__c,
|
||||||
|
Destination_Root_Folder_Href__c,
|
||||||
|
Default_Template_Document_Href__c,
|
||||||
|
Default_Destination_Document_Name_Prefix__c
|
||||||
|
FROM CLM_Letter_Definition__mdt
|
||||||
|
WHERE Active__c = true
|
||||||
|
AND Account_Code__c = :account.accountCode
|
||||||
|
ORDER BY Is_Default__c DESC, Sort_Order__c ASC, Letter_Display_Name__c ASC, DeveloperName ASC
|
||||||
|
]) {
|
||||||
|
letters.add(toLetterSettings(row, account));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (letters.isEmpty()) {
|
||||||
|
letters.add(buildFallbackLetterSettings(account));
|
||||||
|
}
|
||||||
|
return letters;
|
||||||
|
}
|
||||||
|
|
||||||
|
@AuraEnabled(cacheable=true)
|
||||||
|
public static LetterSettings getLetterSettings(String accountCode, String letterCode) {
|
||||||
|
AccountSettings account = getAccountSettings(accountCode);
|
||||||
|
if (account == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String normalizedLetterCode = String.isBlank(letterCode) ? null : letterCode.trim();
|
||||||
|
if (String.isNotBlank(normalizedLetterCode)) {
|
||||||
|
List<CLM_Letter_Definition__mdt> rows = [
|
||||||
|
SELECT DeveloperName,
|
||||||
|
Account_Code__c,
|
||||||
|
Letter_Code__c,
|
||||||
|
Letter_Display_Name__c,
|
||||||
|
Description__c,
|
||||||
|
Active__c,
|
||||||
|
Is_Default__c,
|
||||||
|
Sort_Order__c,
|
||||||
|
Template_Root_Folder_Href__c,
|
||||||
|
Destination_Root_Folder_Href__c,
|
||||||
|
Default_Template_Document_Href__c,
|
||||||
|
Default_Destination_Document_Name_Prefix__c
|
||||||
|
FROM CLM_Letter_Definition__mdt
|
||||||
|
WHERE Active__c = true
|
||||||
|
AND Account_Code__c = :account.accountCode
|
||||||
|
AND Letter_Code__c = :normalizedLetterCode
|
||||||
|
LIMIT 1
|
||||||
|
];
|
||||||
|
if (!rows.isEmpty()) {
|
||||||
|
return toLetterSettings(rows[0], account);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<LetterSettings> letters = listLetterSettings(account.accountCode);
|
||||||
|
for (LetterSettings letter : letters) {
|
||||||
|
if (letter.isDefault) {
|
||||||
|
return letter;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return letters.isEmpty() ? buildFallbackLetterSettings(account) : letters[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
@AuraEnabled(cacheable=false)
|
||||||
|
public static CLMDocGenCallout.CLMDocGenResponse generateDocument(
|
||||||
|
Id appraiserCaseId,
|
||||||
|
String templateDocHref,
|
||||||
|
String destinationFolderHref,
|
||||||
|
String destinationDocName,
|
||||||
|
String accountCode
|
||||||
|
) {
|
||||||
|
CLM_Account_Setting__mdt account = requireAccountSetting(accountCode);
|
||||||
|
CLMDocGenCallout.CLMDocGenResponse response = CLMDocGenCallout.generateDocument(
|
||||||
|
(String) appraiserCaseId,
|
||||||
|
templateDocHref,
|
||||||
|
destinationFolderHref,
|
||||||
|
destinationDocName,
|
||||||
|
account.Environment_Code__c,
|
||||||
|
account.CLM_Account_Id__c,
|
||||||
|
account.CLM_Api_Named_Credential__c
|
||||||
|
);
|
||||||
|
persistDocGenResult(appraiserCaseId, templateDocHref, destinationFolderHref, response, false, accountCode);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
@AuraEnabled(cacheable=false)
|
||||||
|
public static CLMDocGenCallout.CLMDocGenResponse getTaskStatus(Id appraiserCaseId, String taskId, String accountCode) {
|
||||||
|
CLM_Account_Setting__mdt account = requireAccountSetting(accountCode);
|
||||||
|
CLMDocGenCallout.CLMDocGenResponse response = CLMDocGenCallout.getTaskStatus(
|
||||||
|
taskId,
|
||||||
|
account.Environment_Code__c,
|
||||||
|
account.CLM_Account_Id__c,
|
||||||
|
account.CLM_Api_Named_Credential__c
|
||||||
|
);
|
||||||
|
persistDocGenResult(appraiserCaseId, null, null, response, true, accountCode);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
@AuraEnabled(cacheable=false)
|
||||||
|
public static String probeResource(String resourceOrHref, String accountCode) {
|
||||||
|
CLM_Account_Setting__mdt account = requireAccountSetting(accountCode);
|
||||||
|
return performGet(resourceOrHref, account).getBody();
|
||||||
|
}
|
||||||
|
|
||||||
|
@AuraEnabled(cacheable=false)
|
||||||
|
public static DocGenPreview getDocGenPreview(Id appraiserCaseId, String accountCode, String letterCode) {
|
||||||
|
if (appraiserCaseId == null) {
|
||||||
|
throw new AuraHandledException('appraiserCaseId is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
AccountSettings account = getAccountSettings(accountCode);
|
||||||
|
if (account == null) {
|
||||||
|
throw new AuraHandledException('No active CLM account setting was found for ' + accountCode + '.');
|
||||||
|
}
|
||||||
|
|
||||||
|
LetterSettings letter = getLetterSettings(account.accountCode, letterCode);
|
||||||
|
List<Appraiser_Case__c> previewRows = [
|
||||||
|
SELECT Id, Name
|
||||||
|
FROM Appraiser_Case__c
|
||||||
|
WHERE Id = :appraiserCaseId
|
||||||
|
LIMIT 1
|
||||||
|
];
|
||||||
|
if (previewRows.isEmpty()) {
|
||||||
|
throw new AuraHandledException('Appraiser Case not found: ' + appraiserCaseId);
|
||||||
|
}
|
||||||
|
Appraiser_Case__c appraiserCase = previewRows[0];
|
||||||
|
|
||||||
|
String prefix = letter != null && String.isNotBlank(letter.defaultDocumentNamePrefix)
|
||||||
|
? letter.defaultDocumentNamePrefix
|
||||||
|
: account.defaultDocumentNamePrefix;
|
||||||
|
|
||||||
|
DocGenPreview preview = new DocGenPreview();
|
||||||
|
preview.accountCode = account.accountCode;
|
||||||
|
preview.accountDisplayName = account.accountDisplayName;
|
||||||
|
preview.letterCode = letter != null ? letter.letterCode : 'APPRAISER_REVIEW';
|
||||||
|
preview.letterDisplayName = letter != null ? letter.letterDisplayName : 'Appraiser Review Letter';
|
||||||
|
preview.templateDocHref = letter != null ? letter.defaultTemplateDocumentHref : account.defaultTemplateDocumentHref;
|
||||||
|
preview.destinationFolderHref = letter != null ? letter.destinationRootFolderHref : account.destinationRootFolderHref;
|
||||||
|
preview.destinationDocName = buildDefaultDocumentName(prefix, appraiserCase.Name);
|
||||||
|
preview.mergeTaskEndpointUrl = CLMDocGenCallout.buildDocumentXmlMergeTasksUrl(
|
||||||
|
preview.templateDocHref,
|
||||||
|
preview.destinationFolderHref,
|
||||||
|
account.environment,
|
||||||
|
account.clmAccountId
|
||||||
|
);
|
||||||
|
preview.payloadJson = JSON.serializePretty(AppraiserCasePayloadBuilder.buildPayload((String) appraiserCaseId));
|
||||||
|
preview.dataXml = CLMDocGenCallout.prettyPrintXml(CLMDocGenCallout.buildDataXmlForCase((String) appraiserCaseId));
|
||||||
|
preview.requestBodyJson = CLMDocGenCallout.buildRequestBodyJson(
|
||||||
|
(String) appraiserCaseId,
|
||||||
|
preview.templateDocHref,
|
||||||
|
preview.destinationFolderHref,
|
||||||
|
preview.destinationDocName
|
||||||
|
);
|
||||||
|
return preview;
|
||||||
|
}
|
||||||
|
|
||||||
|
@AuraEnabled(cacheable=false)
|
||||||
|
public static CaseContext getCaseContext(Id appraiserCaseId) {
|
||||||
|
List<Appraiser_Case__c> contextRows = [
|
||||||
|
SELECT Id,
|
||||||
|
Name,
|
||||||
|
Property_Street__c,
|
||||||
|
Property_City__c,
|
||||||
|
Property_State_Province__c,
|
||||||
|
Property_Postal_Code__c,
|
||||||
|
Property_Country__c,
|
||||||
|
Last_DocGen_Status__c,
|
||||||
|
Last_DocGen_Message__c,
|
||||||
|
Last_CLM_Account_Code__c,
|
||||||
|
Last_DocGen_Task_Id__c,
|
||||||
|
Last_DocGen_Task_Url__c,
|
||||||
|
Generated_Document_Url__c,
|
||||||
|
Generated_Document_Id__c,
|
||||||
|
Attached_File_Content_Document_Id__c,
|
||||||
|
Attached_File_Url__c,
|
||||||
|
Last_DocGen_Requested_At__c,
|
||||||
|
Last_DocGen_Completed_At__c,
|
||||||
|
(SELECT Id,
|
||||||
|
Deficiency_Number__c,
|
||||||
|
Description__c,
|
||||||
|
Resolution__c,
|
||||||
|
Reference__c
|
||||||
|
FROM Deficiencies__r
|
||||||
|
ORDER BY Deficiency_Number__c ASC, CreatedDate ASC)
|
||||||
|
FROM Appraiser_Case__c
|
||||||
|
WHERE Id = :appraiserCaseId
|
||||||
|
LIMIT 1
|
||||||
|
];
|
||||||
|
if (contextRows.isEmpty()) {
|
||||||
|
throw new AuraHandledException('Appraiser Case not found: ' + appraiserCaseId);
|
||||||
|
}
|
||||||
|
Appraiser_Case__c appraiserCase = contextRows[0];
|
||||||
|
|
||||||
|
CaseContext context = new CaseContext();
|
||||||
|
context.caseId = appraiserCase.Id;
|
||||||
|
context.caseNumber = appraiserCase.Name;
|
||||||
|
context.propertyAddress = formatAddress(appraiserCase);
|
||||||
|
context.lastDocGenStatus = appraiserCase.Last_DocGen_Status__c;
|
||||||
|
context.lastDocGenMessage = appraiserCase.Last_DocGen_Message__c;
|
||||||
|
context.lastClmAccountCode = appraiserCase.Last_CLM_Account_Code__c;
|
||||||
|
context.lastDocGenTaskId = appraiserCase.Last_DocGen_Task_Id__c;
|
||||||
|
context.lastDocGenTaskUrl = appraiserCase.Last_DocGen_Task_Url__c;
|
||||||
|
context.generatedDocumentUrl = appraiserCase.Generated_Document_Url__c;
|
||||||
|
context.generatedDocumentId = appraiserCase.Generated_Document_Id__c;
|
||||||
|
context.attachedFileContentDocumentId = appraiserCase.Attached_File_Content_Document_Id__c;
|
||||||
|
context.attachedFileUrl = appraiserCase.Attached_File_Url__c;
|
||||||
|
context.lastDocGenRequestedAt = appraiserCase.Last_DocGen_Requested_At__c;
|
||||||
|
context.lastDocGenCompletedAt = appraiserCase.Last_DocGen_Completed_At__c;
|
||||||
|
context.deficiencies = new List<CaseDeficiencyItem>();
|
||||||
|
|
||||||
|
if (appraiserCase.Deficiencies__r != null) {
|
||||||
|
for (Appraiser_Case_Deficiency__c deficiency : appraiserCase.Deficiencies__r) {
|
||||||
|
CaseDeficiencyItem item = new CaseDeficiencyItem();
|
||||||
|
item.recordId = deficiency.Id;
|
||||||
|
item.deficiencyNumber = deficiency.Deficiency_Number__c;
|
||||||
|
item.description = deficiency.Description__c;
|
||||||
|
item.resolution = deficiency.Resolution__c;
|
||||||
|
item.reference = deficiency.Reference__c;
|
||||||
|
context.deficiencies.add(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
@AuraEnabled(cacheable=false)
|
||||||
|
public static FileAttachmentResult attachGeneratedDocumentToCase(Id appraiserCaseId, String accountCode) {
|
||||||
|
if (appraiserCaseId == null) {
|
||||||
|
throw new AuraHandledException('appraiserCaseId is required');
|
||||||
|
}
|
||||||
|
CLM_Account_Setting__mdt account = requireAccountSetting(accountCode);
|
||||||
|
|
||||||
|
Appraiser_Case__c appraiserCase = [
|
||||||
|
SELECT Id,
|
||||||
|
Name,
|
||||||
|
Generated_Document_Url__c,
|
||||||
|
Generated_Document_Id__c
|
||||||
|
FROM Appraiser_Case__c
|
||||||
|
WHERE Id = :appraiserCaseId
|
||||||
|
LIMIT 1
|
||||||
|
];
|
||||||
|
|
||||||
|
if (String.isBlank(appraiserCase.Generated_Document_Url__c)) {
|
||||||
|
throw new AuraHandledException('No generated document is available to attach yet.');
|
||||||
|
}
|
||||||
|
|
||||||
|
CLMDocGenCallout.DownloadedDocument downloaded = CLMDocGenCallout.downloadDocument(
|
||||||
|
appraiserCase.Generated_Document_Url__c,
|
||||||
|
account.Environment_Code__c,
|
||||||
|
account.CLM_Account_Id__c,
|
||||||
|
account.CLM_Download_Named_Credential__c
|
||||||
|
);
|
||||||
|
String fileName = String.isNotBlank(downloaded.fileName)
|
||||||
|
? downloaded.fileName
|
||||||
|
: 'Generated_' + appraiserCase.Name + '.docx';
|
||||||
|
String title = fileName.contains('.')
|
||||||
|
? fileName.substringBeforeLast('.')
|
||||||
|
: fileName;
|
||||||
|
|
||||||
|
ContentVersion version = new ContentVersion(
|
||||||
|
Title = title,
|
||||||
|
PathOnClient = '/' + fileName,
|
||||||
|
VersionData = downloaded.body
|
||||||
|
);
|
||||||
|
insert version;
|
||||||
|
|
||||||
|
version = [
|
||||||
|
SELECT Id, ContentDocumentId
|
||||||
|
FROM ContentVersion
|
||||||
|
WHERE Id = :version.Id
|
||||||
|
LIMIT 1
|
||||||
|
];
|
||||||
|
|
||||||
|
insert new ContentDocumentLink(
|
||||||
|
ContentDocumentId = version.ContentDocumentId,
|
||||||
|
LinkedEntityId = appraiserCase.Id,
|
||||||
|
ShareType = 'V',
|
||||||
|
Visibility = 'AllUsers'
|
||||||
|
);
|
||||||
|
|
||||||
|
String fileUrl = '/lightning/r/ContentDocument/' + version.ContentDocumentId + '/view';
|
||||||
|
update new Appraiser_Case__c(
|
||||||
|
Id = appraiserCase.Id,
|
||||||
|
Last_CLM_Account_Code__c = accountCode,
|
||||||
|
Attached_File_Content_Document_Id__c = version.ContentDocumentId,
|
||||||
|
Attached_File_Url__c = fileUrl
|
||||||
|
);
|
||||||
|
|
||||||
|
FileAttachmentResult result = new FileAttachmentResult();
|
||||||
|
result.success = true;
|
||||||
|
result.message = 'Generated document attached to the case.';
|
||||||
|
result.contentDocumentId = version.ContentDocumentId;
|
||||||
|
result.fileUrl = fileUrl;
|
||||||
|
result.fileTitle = title;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String formatAddress(Appraiser_Case__c appraiserCase) {
|
||||||
|
return AppraiserCasePayloadBuilder.formatMailingAddress(
|
||||||
|
appraiserCase.Property_Street__c,
|
||||||
|
appraiserCase.Property_City__c,
|
||||||
|
appraiserCase.Property_State_Province__c,
|
||||||
|
appraiserCase.Property_Postal_Code__c,
|
||||||
|
appraiserCase.Property_Country__c
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@AuraEnabled(cacheable=false)
|
||||||
|
public static FolderContents getFolderContents(String folderHref, String accountCode) {
|
||||||
|
if (String.isBlank(folderHref)) {
|
||||||
|
throw new IllegalArgumentException('folderHref is required');
|
||||||
|
}
|
||||||
|
CLM_Account_Setting__mdt account = requireAccountSetting(accountCode);
|
||||||
|
|
||||||
|
FolderContents contents = new FolderContents();
|
||||||
|
contents.folder = parseSingleResource(performGet(folderHref, account).getBody(), 'Folder');
|
||||||
|
contents.folders = parseResourceList(performGet(folderHref + '/folders', account).getBody(), 'Folder', folderHref);
|
||||||
|
contents.documents = parseResourceList(performGet(folderHref + '/documents', account).getBody(), 'Document', folderHref);
|
||||||
|
return contents;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static HttpResponse performGet(String resourceOrHref, CLM_Account_Setting__mdt account) {
|
||||||
|
HttpRequest req = new HttpRequest();
|
||||||
|
req.setEndpoint(CLMDocGenCallout.buildEndpointForResource(
|
||||||
|
resourceOrHref,
|
||||||
|
account.CLM_Account_Id__c,
|
||||||
|
account.CLM_Api_Named_Credential__c
|
||||||
|
));
|
||||||
|
req.setMethod('GET');
|
||||||
|
req.setTimeout(CLMDocGenCallout.HTTP_TIMEOUT);
|
||||||
|
|
||||||
|
HttpResponse res = new Http().send(req);
|
||||||
|
Integer statusCode = res.getStatusCode();
|
||||||
|
if (statusCode < 200 || statusCode >= 300) {
|
||||||
|
throw new AuraHandledException('CLM API Error (HTTP ' + statusCode + '): ' + res.getBody());
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
@TestVisible
|
||||||
|
private static ResourceItem parseSingleResource(String body, String defaultType) {
|
||||||
|
Object root = JSON.deserializeUntyped(body);
|
||||||
|
if (!(root instanceof Map<String, Object>)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return parseResource((Map<String, Object>) root, defaultType, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@TestVisible
|
||||||
|
private static List<ResourceItem> parseResourceList(String body, String defaultType, String parentHref) {
|
||||||
|
Object root = JSON.deserializeUntyped(body);
|
||||||
|
List<Object> records = unwrapList(root);
|
||||||
|
List<ResourceItem> items = new List<ResourceItem>();
|
||||||
|
|
||||||
|
for (Object record : records) {
|
||||||
|
if (record instanceof Map<String, Object>) {
|
||||||
|
items.add(parseResource((Map<String, Object>) record, defaultType, parentHref));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<Object> unwrapList(Object root) {
|
||||||
|
if (root instanceof List<Object>) {
|
||||||
|
return (List<Object>) root;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (root instanceof Map<String, Object>) {
|
||||||
|
Map<String, Object> payload = (Map<String, Object>) root;
|
||||||
|
for (String key : new List<String>{ 'Results', 'Items', 'Documents', 'Folders' }) {
|
||||||
|
Object value = payload.get(key);
|
||||||
|
if (value instanceof List<Object>) {
|
||||||
|
return (List<Object>) value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (payload.size() == 1) {
|
||||||
|
for (Object value : payload.values()) {
|
||||||
|
if (value instanceof List<Object>) {
|
||||||
|
return (List<Object>) value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new List<Object>();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ResourceItem parseResource(Map<String, Object> source, String defaultType, String parentHref) {
|
||||||
|
ResourceItem item = new ResourceItem();
|
||||||
|
item.name = firstString(source, new List<String>{ 'Name', 'DisplayName', 'Title', 'Label' });
|
||||||
|
item.href = firstString(source, new List<String>{ 'Href', 'Uri', 'Location' });
|
||||||
|
item.type = firstString(source, new List<String>{ 'Type', 'ObjectType', 'ItemType' });
|
||||||
|
item.parentHref = extractParentHref(source, parentHref);
|
||||||
|
item.rawJson = JSON.serialize(source);
|
||||||
|
|
||||||
|
if (String.isBlank(item.type)) {
|
||||||
|
item.type = defaultType;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (String.isBlank(item.name) && String.isNotBlank(item.href)) {
|
||||||
|
item.name = item.href.substringAfterLast('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String extractParentHref(Map<String, Object> source, String fallbackValue) {
|
||||||
|
Object parentValue = source.get('Parent');
|
||||||
|
if (parentValue instanceof Map<String, Object>) {
|
||||||
|
String href = firstString((Map<String, Object>) parentValue, new List<String>{ 'Href', 'Uri', 'Location' });
|
||||||
|
if (String.isNotBlank(href)) {
|
||||||
|
return href;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fallbackValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String firstString(Map<String, Object> source, List<String> keys) {
|
||||||
|
for (String key : keys) {
|
||||||
|
Object value = source.get(key);
|
||||||
|
if (value != null) {
|
||||||
|
String textValue = String.valueOf(value);
|
||||||
|
if (String.isNotBlank(textValue)) {
|
||||||
|
return textValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void persistDocGenResult(
|
||||||
|
Id appraiserCaseId,
|
||||||
|
String templateDocHref,
|
||||||
|
String destinationFolderHref,
|
||||||
|
CLMDocGenCallout.CLMDocGenResponse response,
|
||||||
|
Boolean isStatusRefresh,
|
||||||
|
String accountCode
|
||||||
|
) {
|
||||||
|
if (appraiserCaseId == null || response == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Appraiser_Case__c updateCase = new Appraiser_Case__c(Id = appraiserCaseId);
|
||||||
|
updateCase.Last_CLM_Account_Code__c = accountCode;
|
||||||
|
updateCase.Last_DocGen_Status__c = String.isNotBlank(response.taskStatus) ? response.taskStatus : (response.success ? 'Submitted' : 'Failed');
|
||||||
|
updateCase.Last_DocGen_Message__c = response.message;
|
||||||
|
updateCase.Last_DocGen_Task_Id__c = response.documentId;
|
||||||
|
updateCase.Last_DocGen_Task_Url__c = response.documentUrl;
|
||||||
|
|
||||||
|
if (!isStatusRefresh) {
|
||||||
|
updateCase.Last_DocGen_Requested_At__c = System.now();
|
||||||
|
updateCase.Last_Template_Document_Href__c = templateDocHref;
|
||||||
|
updateCase.Last_Destination_Folder_Href__c = destinationFolderHref;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (String.isNotBlank(response.generatedDocumentUrl)) {
|
||||||
|
updateCase.Generated_Document_Url__c = response.generatedDocumentUrl;
|
||||||
|
updateCase.Generated_Document_Id__c = response.generatedDocumentId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.success && String.valueOf(response.taskStatus).toLowerCase() == 'completed') {
|
||||||
|
updateCase.Last_DocGen_Completed_At__c = System.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
update updateCase;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static CLM_Account_Setting__mdt requireAccountSetting(String accountCode) {
|
||||||
|
CLM_Account_Setting__mdt row = resolveAccountSetting(accountCode);
|
||||||
|
if (row == null) {
|
||||||
|
throw new AuraHandledException('No active CLM account setting was found for ' + accountCode + '.');
|
||||||
|
}
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static CLM_Account_Setting__mdt resolveAccountSetting(String accountCode) {
|
||||||
|
String normalizedCode = String.isBlank(accountCode) ? null : accountCode.trim();
|
||||||
|
List<CLM_Account_Setting__mdt> rows;
|
||||||
|
if (String.isNotBlank(normalizedCode)) {
|
||||||
|
rows = [
|
||||||
|
SELECT DeveloperName,
|
||||||
|
Account_Code__c,
|
||||||
|
Account_Display_Name__c,
|
||||||
|
Environment_Code__c,
|
||||||
|
CLM_Account_Id__c,
|
||||||
|
CLM_Api_Named_Credential__c,
|
||||||
|
CLM_Download_Named_Credential__c,
|
||||||
|
ESignature_Rest_Named_Credential__c,
|
||||||
|
Template_Root_Folder_Href__c,
|
||||||
|
Destination_Root_Folder_Href__c,
|
||||||
|
Default_Template_Document_Href__c,
|
||||||
|
Default_Destination_Document_Name_Prefix__c,
|
||||||
|
Active__c
|
||||||
|
FROM CLM_Account_Setting__mdt
|
||||||
|
WHERE Active__c = true
|
||||||
|
AND DeveloperName = :normalizedCode
|
||||||
|
LIMIT 1
|
||||||
|
];
|
||||||
|
if (rows.isEmpty()) {
|
||||||
|
rows = [
|
||||||
|
SELECT DeveloperName,
|
||||||
|
Account_Code__c,
|
||||||
|
Account_Display_Name__c,
|
||||||
|
Environment_Code__c,
|
||||||
|
CLM_Account_Id__c,
|
||||||
|
CLM_Api_Named_Credential__c,
|
||||||
|
CLM_Download_Named_Credential__c,
|
||||||
|
ESignature_Rest_Named_Credential__c,
|
||||||
|
Template_Root_Folder_Href__c,
|
||||||
|
Destination_Root_Folder_Href__c,
|
||||||
|
Default_Template_Document_Href__c,
|
||||||
|
Default_Destination_Document_Name_Prefix__c,
|
||||||
|
Active__c
|
||||||
|
FROM CLM_Account_Setting__mdt
|
||||||
|
WHERE Active__c = true
|
||||||
|
AND Account_Code__c = :normalizedCode
|
||||||
|
LIMIT 1
|
||||||
|
];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
rows = [
|
||||||
|
SELECT DeveloperName,
|
||||||
|
Account_Code__c,
|
||||||
|
Account_Display_Name__c,
|
||||||
|
Environment_Code__c,
|
||||||
|
CLM_Account_Id__c,
|
||||||
|
CLM_Api_Named_Credential__c,
|
||||||
|
CLM_Download_Named_Credential__c,
|
||||||
|
ESignature_Rest_Named_Credential__c,
|
||||||
|
Template_Root_Folder_Href__c,
|
||||||
|
Destination_Root_Folder_Href__c,
|
||||||
|
Default_Template_Document_Href__c,
|
||||||
|
Default_Destination_Document_Name_Prefix__c,
|
||||||
|
Active__c
|
||||||
|
FROM CLM_Account_Setting__mdt
|
||||||
|
WHERE Active__c = true
|
||||||
|
ORDER BY Account_Display_Name__c ASC, DeveloperName ASC
|
||||||
|
LIMIT 1
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return rows.isEmpty() ? null : rows[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
private static AccountSettings toAccountSettings(CLM_Account_Setting__mdt row) {
|
||||||
|
AccountSettings settings = new AccountSettings();
|
||||||
|
settings.accountCode = String.isNotBlank(row.Account_Code__c) ? row.Account_Code__c : row.DeveloperName;
|
||||||
|
settings.accountDisplayName = String.isNotBlank(row.Account_Display_Name__c) ? row.Account_Display_Name__c : row.DeveloperName;
|
||||||
|
settings.environment = row.Environment_Code__c;
|
||||||
|
settings.clmAccountId = row.CLM_Account_Id__c;
|
||||||
|
settings.clmApiNamedCredential = row.CLM_Api_Named_Credential__c;
|
||||||
|
settings.clmDownloadNamedCredential = row.CLM_Download_Named_Credential__c;
|
||||||
|
settings.eSignatureRestNamedCredential = row.ESignature_Rest_Named_Credential__c;
|
||||||
|
settings.templateRootFolderHref = row.Template_Root_Folder_Href__c;
|
||||||
|
settings.destinationRootFolderHref = row.Destination_Root_Folder_Href__c;
|
||||||
|
settings.defaultTemplateDocumentHref = row.Default_Template_Document_Href__c;
|
||||||
|
settings.defaultDocumentNamePrefix = row.Default_Destination_Document_Name_Prefix__c;
|
||||||
|
settings.active = row.Active__c;
|
||||||
|
return settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static LetterSettings toLetterSettings(CLM_Letter_Definition__mdt row, AccountSettings account) {
|
||||||
|
LetterSettings settings = new LetterSettings();
|
||||||
|
settings.accountCode = String.isNotBlank(row.Account_Code__c) ? row.Account_Code__c : account.accountCode;
|
||||||
|
settings.letterCode = String.isNotBlank(row.Letter_Code__c) ? row.Letter_Code__c : 'APPRAISER_REVIEW';
|
||||||
|
settings.letterDisplayName = String.isNotBlank(row.Letter_Display_Name__c) ? row.Letter_Display_Name__c : row.DeveloperName;
|
||||||
|
settings.description = row.Description__c;
|
||||||
|
settings.isDefault = row.Is_Default__c;
|
||||||
|
settings.sortOrder = row.Sort_Order__c;
|
||||||
|
settings.templateRootFolderHref = firstNonBlankValue(row.Template_Root_Folder_Href__c, account.templateRootFolderHref);
|
||||||
|
settings.destinationRootFolderHref = firstNonBlankValue(row.Destination_Root_Folder_Href__c, account.destinationRootFolderHref);
|
||||||
|
settings.defaultTemplateDocumentHref = firstNonBlankValue(row.Default_Template_Document_Href__c, account.defaultTemplateDocumentHref);
|
||||||
|
settings.defaultDocumentNamePrefix = firstNonBlankValue(row.Default_Destination_Document_Name_Prefix__c, account.defaultDocumentNamePrefix);
|
||||||
|
settings.active = row.Active__c;
|
||||||
|
return settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static LetterSettings buildFallbackLetterSettings(AccountSettings account) {
|
||||||
|
LetterSettings settings = new LetterSettings();
|
||||||
|
settings.accountCode = account.accountCode;
|
||||||
|
settings.letterCode = 'APPRAISER_REVIEW';
|
||||||
|
settings.letterDisplayName = 'Appraiser Review Letter';
|
||||||
|
settings.description = 'Fallback current appraiser letter flow.';
|
||||||
|
settings.isDefault = true;
|
||||||
|
settings.sortOrder = 10;
|
||||||
|
settings.templateRootFolderHref = account.templateRootFolderHref;
|
||||||
|
settings.destinationRootFolderHref = account.destinationRootFolderHref;
|
||||||
|
settings.defaultTemplateDocumentHref = account.defaultTemplateDocumentHref;
|
||||||
|
settings.defaultDocumentNamePrefix = account.defaultDocumentNamePrefix;
|
||||||
|
settings.active = true;
|
||||||
|
return settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String firstNonBlankValue(String preferredValue, String fallbackValue) {
|
||||||
|
return String.isNotBlank(preferredValue) ? preferredValue : fallbackValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String buildDefaultDocumentName(String prefix, String caseNumber) {
|
||||||
|
String normalizedPrefix = String.isNotBlank(prefix) ? prefix : 'Review';
|
||||||
|
return String.isNotBlank(caseNumber)
|
||||||
|
? normalizedPrefix + '_' + caseNumber + '.docx'
|
||||||
|
: normalizedPrefix + '.docx';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
|
||||||
|
<apiVersion>63.0</apiVersion>
|
||||||
|
<status>Active</status>
|
||||||
|
</ApexClass>
|
||||||
|
|
@ -0,0 +1,286 @@
|
||||||
|
@IsTest
|
||||||
|
private class CLMAdminServiceTest {
|
||||||
|
private class FolderBrowseMock implements HttpCalloutMock {
|
||||||
|
public HttpResponse respond(HttpRequest req) {
|
||||||
|
HttpResponse res = new HttpResponse();
|
||||||
|
res.setHeader('Content-Type', 'application/json');
|
||||||
|
|
||||||
|
if (req.getMethod() == 'POST' && req.getEndpoint().contains('/documentxmlmergetasks')) {
|
||||||
|
res.setStatusCode(200);
|
||||||
|
res.setBody('{"Href":"https://apiuatna11.springcm.com/v2/bccae332-c7db-4892-ab85-257df0f70fea/documentxmlmergetasks/TASK-555","Status":"Queued","Result":{"Href":"https://apiuatna11.springcm.com/v2/bccae332-c7db-4892-ab85-257df0f70fea/documents/generated-555"}}');
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.getEndpoint().endsWith('/folders/root-folder')) {
|
||||||
|
res.setStatusCode(200);
|
||||||
|
res.setBody('{"Name":"Templates Root","Href":"https://uatna11.springcm.com/v2/2371cf36-eb8a-43fe-9f28-b5bbe7644397/folders/root-folder","Type":"Folder","Parent":{"Href":"https://uatna11.springcm.com/v2/2371cf36-eb8a-43fe-9f28-b5bbe7644397/folders/root-parent"}}');
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.getEndpoint().endsWith('/folders/root-folder/folders')) {
|
||||||
|
res.setStatusCode(200);
|
||||||
|
res.setBody('{"Results":[{"Name":"Residential","Href":"https://uatna11.springcm.com/v2/2371cf36-eb8a-43fe-9f28-b5bbe7644397/folders/residential","Parent":{"Href":"https://uatna11.springcm.com/v2/2371cf36-eb8a-43fe-9f28-b5bbe7644397/folders/root-folder"}}]}');
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.getEndpoint().endsWith('/folders/root-folder/documents')) {
|
||||||
|
res.setStatusCode(200);
|
||||||
|
res.setBody('{"Results":[{"Name":"Appraiser Review Letter Template","Href":"https://uatna11.springcm.com/v2/2371cf36-eb8a-43fe-9f28-b5bbe7644397/documents/template-1"}]}');
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.getMethod() == 'GET' && req.getEndpoint().contains('/documentxmlmergetasks')) {
|
||||||
|
res.setStatusCode(200);
|
||||||
|
res.setBody('{"Href":"https://apiuatna11.springcm.com/v2/bccae332-c7db-4892-ab85-257df0f70fea/documentxmlmergetasks/TASK-555","Status":"Completed","Result":{"Href":"https://apiuatna11.springcm.com/v2/bccae332-c7db-4892-ab85-257df0f70fea/documents/generated-555"}}');
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.getMethod() == 'GET' && req.getEndpoint().contains('/documents/generated-555')) {
|
||||||
|
res.setStatusCode(200);
|
||||||
|
res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document');
|
||||||
|
res.setHeader('Content-Disposition', 'attachment; filename="Review_AC-000001.docx"');
|
||||||
|
res.setBodyAsBlob(Blob.valueOf('docx-binary'));
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.setStatusCode(404);
|
||||||
|
res.setBody('{"Message":"Not Found"}');
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@IsTest
|
||||||
|
static void parsesFolderContents() {
|
||||||
|
Test.setMock(HttpCalloutMock.class, new FolderBrowseMock());
|
||||||
|
|
||||||
|
Test.startTest();
|
||||||
|
CLMAdminService.FolderContents contents = CLMAdminService.getFolderContents(
|
||||||
|
'https://uatna11.springcm.com/v2/2371cf36-eb8a-43fe-9f28-b5bbe7644397/folders/root-folder',
|
||||||
|
'DTC_CLM_Demo'
|
||||||
|
);
|
||||||
|
String body = CLMAdminService.probeResource('/documentxmlmergetasks/TASK-555', 'DTC_IAM_Enterprise');
|
||||||
|
Test.stopTest();
|
||||||
|
|
||||||
|
System.assertEquals('Templates Root', contents.folder.name);
|
||||||
|
System.assertEquals('https://uatna11.springcm.com/v2/2371cf36-eb8a-43fe-9f28-b5bbe7644397/folders/root-parent', contents.folder.parentHref);
|
||||||
|
System.assertEquals(1, contents.folders.size());
|
||||||
|
System.assertEquals('Residential', contents.folders[0].name);
|
||||||
|
System.assertEquals(1, contents.documents.size());
|
||||||
|
System.assertEquals('Appraiser Review Letter Template', contents.documents[0].name);
|
||||||
|
System.assert(body.contains('Completed'));
|
||||||
|
}
|
||||||
|
|
||||||
|
@IsTest
|
||||||
|
static void returnsAccountSettingsFromMetadata() {
|
||||||
|
Test.startTest();
|
||||||
|
List<CLMAdminService.AccountSettings> accounts = CLMAdminService.listAccountSettings();
|
||||||
|
CLMAdminService.AccountSettings settings = CLMAdminService.getAccountSettings('DTC_CLM_Demo');
|
||||||
|
Test.stopTest();
|
||||||
|
|
||||||
|
Set<String> accountCodes = new Set<String>();
|
||||||
|
for (CLMAdminService.AccountSettings account : accounts) {
|
||||||
|
accountCodes.add(account.accountCode);
|
||||||
|
}
|
||||||
|
System.assert(accountCodes.contains('DTC_CLM_Demo'));
|
||||||
|
System.assert(accountCodes.contains('DTC_IAM_Enterprise'));
|
||||||
|
System.assert(accountCodes.contains('DTC_HUD_Demo'));
|
||||||
|
System.assertNotEquals(null, settings);
|
||||||
|
System.assertEquals('DTC_CLM_Demo', settings.accountCode);
|
||||||
|
System.assertEquals('DTC CLM Demo', settings.accountDisplayName);
|
||||||
|
System.assertEquals('UAT', settings.environment);
|
||||||
|
System.assert(settings.destinationRootFolderHref.contains('/folders/'));
|
||||||
|
System.assert(settings.defaultTemplateDocumentHref.contains('/documents/'));
|
||||||
|
System.assertEquals('Review', settings.defaultDocumentNamePrefix);
|
||||||
|
}
|
||||||
|
|
||||||
|
@IsTest
|
||||||
|
static void returnsLetterSettingsFromMetadata() {
|
||||||
|
Test.startTest();
|
||||||
|
List<CLMAdminService.LetterSettings> letters = CLMAdminService.listLetterSettings('DTC_CLM_Demo');
|
||||||
|
CLMAdminService.LetterSettings settings = CLMAdminService.getLetterSettings('DTC_CLM_Demo', 'APPRAISER_REVIEW');
|
||||||
|
Test.stopTest();
|
||||||
|
|
||||||
|
System.assert(!letters.isEmpty());
|
||||||
|
System.assertEquals('APPRAISER_REVIEW', settings.letterCode);
|
||||||
|
System.assertEquals('Appraiser Review Letter', settings.letterDisplayName);
|
||||||
|
System.assertEquals(true, settings.isDefault);
|
||||||
|
System.assertEquals('Review', settings.defaultDocumentNamePrefix);
|
||||||
|
}
|
||||||
|
|
||||||
|
@IsTest
|
||||||
|
static void buildsDocGenPreviewForSelectedLetter() {
|
||||||
|
Appraiser_Case__c appraiserCase = new Appraiser_Case__c(
|
||||||
|
Appraiser_Field_Review_Date__c = Date.today(),
|
||||||
|
Letter_Sent_Date__c = Date.newInstance(2026, 4, 9),
|
||||||
|
FHA_Case_Number__c = '123-4567890',
|
||||||
|
Appraiser_Name__c = 'Jamie Carter',
|
||||||
|
Appraiser_Last_Name__c = 'Carter',
|
||||||
|
Appraiser_Street__c = '12 Park Ave',
|
||||||
|
Appraiser_City__c = 'New York',
|
||||||
|
Appraiser_State_Province__c = 'NY',
|
||||||
|
Appraiser_Postal_Code__c = '10016',
|
||||||
|
Appraiser_Country__c = 'USA',
|
||||||
|
Property_Street__c = '245 Lexington Ave',
|
||||||
|
Property_City__c = 'New York',
|
||||||
|
Property_State_Province__c = 'NY',
|
||||||
|
Property_Postal_Code__c = '10016',
|
||||||
|
Property_Country__c = 'USA'
|
||||||
|
);
|
||||||
|
insert appraiserCase;
|
||||||
|
|
||||||
|
insert new Appraiser_Case_Deficiency__c(
|
||||||
|
Appraiser_Case__c = appraiserCase.Id,
|
||||||
|
Deficiency_Number__c = 1,
|
||||||
|
Description__c = 'Test deficiency',
|
||||||
|
Resolution__c = 'Test resolution',
|
||||||
|
Reference__c = 'VC-1'
|
||||||
|
);
|
||||||
|
|
||||||
|
Test.startTest();
|
||||||
|
CLMAdminService.DocGenPreview preview = CLMAdminService.getDocGenPreview(
|
||||||
|
appraiserCase.Id,
|
||||||
|
'DTC_CLM_Demo',
|
||||||
|
'NOD_LETTER'
|
||||||
|
);
|
||||||
|
Test.stopTest();
|
||||||
|
|
||||||
|
System.assertEquals('DTC_CLM_Demo', preview.accountCode);
|
||||||
|
System.assertEquals('NOD_LETTER', preview.letterCode);
|
||||||
|
System.assertEquals('NOD Letter', preview.letterDisplayName);
|
||||||
|
Appraiser_Case__c refreshedCase = [
|
||||||
|
SELECT Name
|
||||||
|
FROM Appraiser_Case__c
|
||||||
|
WHERE Id = :appraiserCase.Id
|
||||||
|
LIMIT 1
|
||||||
|
];
|
||||||
|
System.assertEquals('NOD_' + refreshedCase.Name + '.docx', preview.destinationDocName);
|
||||||
|
System.assert(preview.mergeTaskEndpointUrl.endsWith('/documentxmlmergetasks'));
|
||||||
|
System.assert(preview.payloadJson.contains('FHACaseNumber'));
|
||||||
|
System.assert(preview.dataXml.contains('\n <'));
|
||||||
|
System.assert(preview.dataXml.contains('<Reference>VC-1</Reference>'));
|
||||||
|
System.assert(preview.requestBodyJson.contains('"DestinationDocumentName"'));
|
||||||
|
}
|
||||||
|
|
||||||
|
@IsTest
|
||||||
|
static void persistsCaseTrackingOnGenerate() {
|
||||||
|
Appraiser_Case__c appraiserCase = new Appraiser_Case__c(
|
||||||
|
Appraiser_Field_Review_Date__c = Date.today(),
|
||||||
|
Property_Street__c = '123 Main St',
|
||||||
|
Property_City__c = 'Denver',
|
||||||
|
Property_State_Province__c = 'CO',
|
||||||
|
Property_Postal_Code__c = '80202'
|
||||||
|
);
|
||||||
|
insert appraiserCase;
|
||||||
|
|
||||||
|
insert new Appraiser_Case_Deficiency__c(
|
||||||
|
Appraiser_Case__c = appraiserCase.Id,
|
||||||
|
Deficiency_Number__c = 1,
|
||||||
|
Description__c = 'Test deficiency',
|
||||||
|
Resolution__c = 'Test resolution',
|
||||||
|
Reference__c = 'VC-1'
|
||||||
|
);
|
||||||
|
|
||||||
|
Test.setMock(HttpCalloutMock.class, new FolderBrowseMock());
|
||||||
|
|
||||||
|
Test.startTest();
|
||||||
|
CLMDocGenCallout.CLMDocGenResponse generateResponse = CLMAdminService.generateDocument(
|
||||||
|
appraiserCase.Id,
|
||||||
|
'https://apiuatna11.springcm.com/v2/bccae332-c7db-4892-ab85-257df0f70fea/documents/template-1',
|
||||||
|
'https://apiuatna11.springcm.com/v2/bccae332-c7db-4892-ab85-257df0f70fea/folders/root-folder',
|
||||||
|
'Review_AC-000001.docx',
|
||||||
|
'DTC_CLM_Demo'
|
||||||
|
);
|
||||||
|
CLMAdminService.CaseContext context = CLMAdminService.getCaseContext(appraiserCase.Id);
|
||||||
|
Test.stopTest();
|
||||||
|
|
||||||
|
System.assertEquals(true, generateResponse.success);
|
||||||
|
System.assertEquals('Queued', context.lastDocGenStatus);
|
||||||
|
System.assertEquals('DTC_CLM_Demo', context.lastClmAccountCode);
|
||||||
|
System.assertEquals('TASK-555', context.lastDocGenTaskId);
|
||||||
|
System.assert(context.lastDocGenTaskUrl.contains('/documentxmlmergetasks/TASK-555'));
|
||||||
|
System.assert(context.generatedDocumentUrl.contains('/documents/generated-555'));
|
||||||
|
System.assertNotEquals(null, context.lastDocGenRequestedAt);
|
||||||
|
System.assertEquals(null, context.lastDocGenCompletedAt);
|
||||||
|
System.assertEquals(1, context.deficiencies.size());
|
||||||
|
System.assertEquals('123 Main St, Denver, CO 80202', context.propertyAddress);
|
||||||
|
System.assertEquals('VC-1', context.deficiencies[0].reference);
|
||||||
|
}
|
||||||
|
|
||||||
|
@IsTest
|
||||||
|
static void persistsCaseTrackingOnStatusRefresh() {
|
||||||
|
Appraiser_Case__c appraiserCase = new Appraiser_Case__c(
|
||||||
|
Appraiser_Field_Review_Date__c = Date.today(),
|
||||||
|
Property_Street__c = '123 Main St',
|
||||||
|
Property_City__c = 'Denver',
|
||||||
|
Property_State_Province__c = 'CO',
|
||||||
|
Property_Postal_Code__c = '80202',
|
||||||
|
Last_DocGen_Task_Id__c = 'TASK-555'
|
||||||
|
);
|
||||||
|
insert appraiserCase;
|
||||||
|
|
||||||
|
insert new Appraiser_Case_Deficiency__c(
|
||||||
|
Appraiser_Case__c = appraiserCase.Id,
|
||||||
|
Deficiency_Number__c = 1,
|
||||||
|
Description__c = 'Test deficiency',
|
||||||
|
Resolution__c = 'Test resolution',
|
||||||
|
Reference__c = 'VC-1'
|
||||||
|
);
|
||||||
|
|
||||||
|
Test.setMock(HttpCalloutMock.class, new FolderBrowseMock());
|
||||||
|
|
||||||
|
Test.startTest();
|
||||||
|
CLMDocGenCallout.CLMDocGenResponse statusResponse = CLMAdminService.getTaskStatus(appraiserCase.Id, 'TASK-555', 'DTC_CLM_Demo');
|
||||||
|
CLMAdminService.CaseContext context = CLMAdminService.getCaseContext(appraiserCase.Id);
|
||||||
|
Test.stopTest();
|
||||||
|
|
||||||
|
System.assertEquals(true, statusResponse.success);
|
||||||
|
System.assertEquals('Completed', context.lastDocGenStatus);
|
||||||
|
System.assertEquals('DTC_CLM_Demo', context.lastClmAccountCode);
|
||||||
|
System.assertEquals('TASK-555', context.lastDocGenTaskId);
|
||||||
|
System.assert(context.lastDocGenTaskUrl.contains('/documentxmlmergetasks/TASK-555'));
|
||||||
|
System.assert(context.generatedDocumentUrl.contains('/documents/generated-555'));
|
||||||
|
System.assertNotEquals(null, context.lastDocGenCompletedAt);
|
||||||
|
System.assertEquals(1, context.deficiencies.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
@IsTest
|
||||||
|
static void attachesGeneratedDocumentToCaseAsSalesforceFile() {
|
||||||
|
Appraiser_Case__c appraiserCase = new Appraiser_Case__c(
|
||||||
|
Appraiser_Field_Review_Date__c = Date.today(),
|
||||||
|
Property_Street__c = '245 Lexington Ave',
|
||||||
|
Property_City__c = 'New York',
|
||||||
|
Property_State_Province__c = 'NY',
|
||||||
|
Property_Postal_Code__c = '10016',
|
||||||
|
Generated_Document_Url__c = 'https://apiuatna11.springcm.com/v2/bccae332-c7db-4892-ab85-257df0f70fea/documents/generated-555',
|
||||||
|
Generated_Document_Id__c = 'generated-555'
|
||||||
|
);
|
||||||
|
insert appraiserCase;
|
||||||
|
|
||||||
|
Test.setMock(HttpCalloutMock.class, new FolderBrowseMock());
|
||||||
|
|
||||||
|
Test.startTest();
|
||||||
|
CLMAdminService.FileAttachmentResult result = CLMAdminService.attachGeneratedDocumentToCase(appraiserCase.Id, 'DTC_CLM_Demo');
|
||||||
|
Test.stopTest();
|
||||||
|
|
||||||
|
Appraiser_Case__c refreshedCase = [
|
||||||
|
SELECT Last_CLM_Account_Code__c, Attached_File_Content_Document_Id__c, Attached_File_Url__c
|
||||||
|
FROM Appraiser_Case__c
|
||||||
|
WHERE Id = :appraiserCase.Id
|
||||||
|
LIMIT 1
|
||||||
|
];
|
||||||
|
List<ContentDocumentLink> links = [
|
||||||
|
SELECT ContentDocumentId, LinkedEntityId
|
||||||
|
FROM ContentDocumentLink
|
||||||
|
WHERE LinkedEntityId = :appraiserCase.Id
|
||||||
|
];
|
||||||
|
|
||||||
|
System.assertEquals(true, result.success);
|
||||||
|
System.assertNotEquals(null, result.contentDocumentId);
|
||||||
|
System.assert(result.fileUrl.contains('/lightning/r/ContentDocument/'));
|
||||||
|
System.assertEquals('DTC_CLM_Demo', refreshedCase.Last_CLM_Account_Code__c);
|
||||||
|
System.assertEquals(result.contentDocumentId, refreshedCase.Attached_File_Content_Document_Id__c);
|
||||||
|
System.assertEquals(result.fileUrl, refreshedCase.Attached_File_Url__c);
|
||||||
|
System.assertEquals(1, links.size());
|
||||||
|
System.assertEquals(result.contentDocumentId, links[0].ContentDocumentId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
|
||||||
|
<apiVersion>63.0</apiVersion>
|
||||||
|
<status>Active</status>
|
||||||
|
</ApexClass>
|
||||||
|
|
@ -1,18 +1,95 @@
|
||||||
public class CLMDocGenCallout {
|
public class CLMDocGenCallout {
|
||||||
|
|
||||||
// S1 demo environment
|
// S1 demo environment
|
||||||
private static final String CLM_ACCOUNT_ID_S1 = '2371cf36-eb8a-43fe-9f28-b5bbe7644397';
|
private static final String CLM_ACCOUNT_ID_S1 = '2371cf36-eb8a-43fe-9f28-b5bbe7644397';
|
||||||
private static final String CLM_BASE_S1 = 'callout:CLMNamedCred/v2/' + CLM_ACCOUNT_ID_S1;
|
private static final String CLM_NAMED_CRED_S1 = 'callout:CLMs1NamedCreds';
|
||||||
|
private static final String CLM_DOWNLOAD_NAMED_CRED_S1 = 'callout:CLMs1Download';
|
||||||
|
|
||||||
// UAT demo environment
|
// UAT demo environment
|
||||||
private static final String CLM_ACCOUNT_ID_UAT = 'bccae332-c7db-4892-ab85-257df0f70fea';
|
private static final String CLM_ACCOUNT_ID_UAT = 'bccae332-c7db-4892-ab85-257df0f70fea';
|
||||||
private static final String CLM_BASE_UAT = 'callout:CLMuatNamedCreds/v2/' + CLM_ACCOUNT_ID_UAT;
|
private static final String CLM_NAMED_CRED_UAT = 'callout:CLMuatNamedCreds';
|
||||||
|
private static final String CLM_DOWNLOAD_NAMED_CRED_UAT = 'callout:CLMuatDownload';
|
||||||
|
|
||||||
private static final Integer HTTP_TIMEOUT = 30000;
|
public static final Integer HTTP_TIMEOUT = 30000;
|
||||||
|
|
||||||
/** Resolve the correct CLM base URL. env: 'UAT' or 'S1'. */
|
/** Resolve the correct CLM Named Credential base. env: 'UAT' or 'S1'. */
|
||||||
private static String clmBase(String env) {
|
private static String clmNamedCredential(String env) {
|
||||||
return env == 'S1' ? CLM_BASE_S1 : CLM_BASE_UAT;
|
return env == 'S1' ? CLM_NAMED_CRED_S1 : CLM_NAMED_CRED_UAT;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String clmDownloadNamedCredential(String env) {
|
||||||
|
return env == 'S1' ? CLM_DOWNLOAD_NAMED_CRED_S1 : CLM_DOWNLOAD_NAMED_CRED_UAT;
|
||||||
|
}
|
||||||
|
|
||||||
|
@TestVisible
|
||||||
|
static String defaultAccountId(String env) {
|
||||||
|
return env == 'S1' ? CLM_ACCOUNT_ID_S1 : CLM_ACCOUNT_ID_UAT;
|
||||||
|
}
|
||||||
|
|
||||||
|
@TestVisible
|
||||||
|
static String extractAccountId(String resourceOrHref) {
|
||||||
|
if (String.isBlank(resourceOrHref)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Integer v2Index = resourceOrHref.indexOf('/v2/');
|
||||||
|
if (v2Index < 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String afterV2 = resourceOrHref.substring(v2Index + 4);
|
||||||
|
List<String> parts = afterV2.split('/');
|
||||||
|
return parts.isEmpty() ? null : parts[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
@TestVisible
|
||||||
|
static String normalizeResourcePath(String resourceOrHref, String env) {
|
||||||
|
return normalizeResourcePathWithAccountId(resourceOrHref, defaultAccountId(env));
|
||||||
|
}
|
||||||
|
|
||||||
|
@TestVisible
|
||||||
|
static String normalizeResourcePathWithAccountId(String resourceOrHref, String fallbackAccountId) {
|
||||||
|
if (String.isBlank(resourceOrHref)) {
|
||||||
|
throw new IllegalArgumentException('resourceOrHref is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resourceOrHref.startsWith('http://') || resourceOrHref.startsWith('https://')) {
|
||||||
|
Integer pathStart = resourceOrHref.indexOf('/', resourceOrHref.indexOf('//') + 2);
|
||||||
|
if (pathStart < 0) {
|
||||||
|
throw new IllegalArgumentException('Unable to determine resource path from href: ' + resourceOrHref);
|
||||||
|
}
|
||||||
|
return resourceOrHref.substring(pathStart);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resourceOrHref.startsWith('/v2/')) {
|
||||||
|
return resourceOrHref;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resourceOrHref.startsWith('v2/')) {
|
||||||
|
return '/' + resourceOrHref;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resourceOrHref.startsWith('/')) {
|
||||||
|
return '/v2/' + fallbackAccountId + resourceOrHref;
|
||||||
|
}
|
||||||
|
|
||||||
|
return '/v2/' + fallbackAccountId + '/' + resourceOrHref;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String buildEndpointForResource(String resourceOrHref, String env) {
|
||||||
|
return clmNamedCredential(env) + normalizeResourcePath(resourceOrHref, env);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String buildEndpointForResource(String resourceOrHref, String accountId, String apiNamedCredential) {
|
||||||
|
return 'callout:' + apiNamedCredential + normalizeResourcePathWithAccountId(resourceOrHref, accountId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String buildDownloadEndpointForResource(String resourceOrHref, String env) {
|
||||||
|
return clmDownloadNamedCredential(env) + normalizeResourcePath(resourceOrHref, env);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String buildDownloadEndpointForResource(String resourceOrHref, String accountId, String downloadNamedCredential) {
|
||||||
|
return 'callout:' + downloadNamedCredential + normalizeResourcePathWithAccountId(resourceOrHref, accountId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Defaults to UAT environment. */
|
/** Defaults to UAT environment. */
|
||||||
|
|
@ -40,21 +117,42 @@ public class CLMDocGenCallout {
|
||||||
String destinationDocName,
|
String destinationDocName,
|
||||||
String env
|
String env
|
||||||
) {
|
) {
|
||||||
Map<String, Object> casePayload = AppraiserCasePayloadBuilder.buildPayload(caseId);
|
return generateDocument(
|
||||||
|
caseId,
|
||||||
|
templateDocHref,
|
||||||
|
destinationFolderHref,
|
||||||
|
destinationDocName,
|
||||||
|
env,
|
||||||
|
defaultAccountId(env),
|
||||||
|
clmNamedCredential(env).substringAfter('callout:')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Map<String, Object> requestBody = new Map<String, Object>{
|
public static CLMDocGenResponse generateDocument(
|
||||||
'TemplateDocument' => new Map<String, Object>{
|
String caseId,
|
||||||
'Href' => templateDocHref
|
String templateDocHref,
|
||||||
},
|
String destinationFolderHref,
|
||||||
'DataXML' => buildDataXml(casePayload),
|
String destinationDocName,
|
||||||
'DestinationDocumentName' => destinationDocName,
|
String env,
|
||||||
'DestinationFolder' => new Map<String, Object>{
|
String configuredAccountId,
|
||||||
'Href' => destinationFolderHref
|
String apiNamedCredential
|
||||||
}
|
) {
|
||||||
};
|
Map<String, Object> casePayload = AppraiserCasePayloadBuilder.buildPayload(caseId);
|
||||||
|
String accountId = firstNonBlank(
|
||||||
|
extractAccountId(templateDocHref),
|
||||||
|
extractAccountId(destinationFolderHref),
|
||||||
|
configuredAccountId,
|
||||||
|
defaultAccountId(env)
|
||||||
|
);
|
||||||
|
Map<String, Object> requestBody = buildRequestBodyMap(
|
||||||
|
casePayload,
|
||||||
|
templateDocHref,
|
||||||
|
destinationFolderHref,
|
||||||
|
destinationDocName
|
||||||
|
);
|
||||||
|
|
||||||
HttpRequest req = new HttpRequest();
|
HttpRequest req = new HttpRequest();
|
||||||
req.setEndpoint(clmBase(env) + '/documentxmlmergetasks');
|
req.setEndpoint('callout:' + apiNamedCredential + '/v2/' + accountId + '/documentxmlmergetasks');
|
||||||
req.setMethod('POST');
|
req.setMethod('POST');
|
||||||
req.setHeader('Content-Type', 'application/json');
|
req.setHeader('Content-Type', 'application/json');
|
||||||
req.setTimeout(HTTP_TIMEOUT);
|
req.setTimeout(HTTP_TIMEOUT);
|
||||||
|
|
@ -69,15 +167,59 @@ public class CLMDocGenCallout {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Poll the status of a submitted merge task by its GUID. */
|
public static String buildDataXmlForCase(String caseId) {
|
||||||
|
return buildDataXml(AppraiserCasePayloadBuilder.buildPayload(caseId));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String buildRequestBodyJson(
|
||||||
|
String caseId,
|
||||||
|
String templateDocHref,
|
||||||
|
String destinationFolderHref,
|
||||||
|
String destinationDocName
|
||||||
|
) {
|
||||||
|
return JSON.serializePretty(
|
||||||
|
buildRequestBodyMap(
|
||||||
|
AppraiserCasePayloadBuilder.buildPayload(caseId),
|
||||||
|
templateDocHref,
|
||||||
|
destinationFolderHref,
|
||||||
|
destinationDocName
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String buildDocumentXmlMergeTasksUrl(
|
||||||
|
String templateDocHref,
|
||||||
|
String destinationFolderHref,
|
||||||
|
String env,
|
||||||
|
String configuredAccountId
|
||||||
|
) {
|
||||||
|
String accountId = firstNonBlank(
|
||||||
|
extractAccountId(templateDocHref),
|
||||||
|
extractAccountId(destinationFolderHref),
|
||||||
|
configuredAccountId,
|
||||||
|
defaultAccountId(env)
|
||||||
|
);
|
||||||
|
String baseUrl = firstNonBlank(
|
||||||
|
extractBaseUrl(templateDocHref),
|
||||||
|
extractBaseUrl(destinationFolderHref),
|
||||||
|
null,
|
||||||
|
defaultBaseUrl(env)
|
||||||
|
);
|
||||||
|
return baseUrl + '/v2/' + accountId + '/documentxmlmergetasks';
|
||||||
|
}
|
||||||
|
|
||||||
/** Poll the status of a submitted merge task by its GUID (defaults to UAT). */
|
/** Poll the status of a submitted merge task by its GUID (defaults to UAT). */
|
||||||
public static CLMDocGenResponse getTaskStatus(String taskId) {
|
public static CLMDocGenResponse getTaskStatus(String taskId) {
|
||||||
return getTaskStatus(taskId, 'UAT');
|
return getTaskStatus(taskId, 'UAT');
|
||||||
}
|
}
|
||||||
|
|
||||||
public static CLMDocGenResponse getTaskStatus(String taskId, String env) {
|
public static CLMDocGenResponse getTaskStatus(String taskId, String env) {
|
||||||
|
return getTaskStatus(taskId, env, defaultAccountId(env), clmNamedCredential(env).substringAfter('callout:'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static CLMDocGenResponse getTaskStatus(String taskId, String env, String configuredAccountId, String apiNamedCredential) {
|
||||||
HttpRequest req = new HttpRequest();
|
HttpRequest req = new HttpRequest();
|
||||||
req.setEndpoint(clmBase(env) + '/documentxmlmergetasks/' + taskId);
|
req.setEndpoint(buildEndpointForResource('/documentxmlmergetasks/' + taskId, configuredAccountId, apiNamedCredential));
|
||||||
req.setMethod('GET');
|
req.setMethod('GET');
|
||||||
req.setTimeout(HTTP_TIMEOUT);
|
req.setTimeout(HTTP_TIMEOUT);
|
||||||
try {
|
try {
|
||||||
|
|
@ -96,13 +238,41 @@ public class CLMDocGenCallout {
|
||||||
|
|
||||||
public static String probe(String resource, String env) {
|
public static String probe(String resource, String env) {
|
||||||
HttpRequest req = new HttpRequest();
|
HttpRequest req = new HttpRequest();
|
||||||
req.setEndpoint(clmBase(env) + '/' + resource);
|
req.setEndpoint(buildEndpointForResource(resource, env));
|
||||||
req.setMethod('GET');
|
req.setMethod('GET');
|
||||||
req.setTimeout(HTTP_TIMEOUT);
|
req.setTimeout(HTTP_TIMEOUT);
|
||||||
HttpResponse res = new Http().send(req);
|
HttpResponse res = new Http().send(req);
|
||||||
return 'HTTP ' + res.getStatusCode() + ': ' + res.getBody();
|
return 'HTTP ' + res.getStatusCode() + ': ' + res.getBody();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static DownloadedDocument downloadDocument(String resourceOrHref, String env) {
|
||||||
|
return downloadDocument(
|
||||||
|
resourceOrHref,
|
||||||
|
env,
|
||||||
|
defaultAccountId(env),
|
||||||
|
clmDownloadNamedCredential(env).substringAfter('callout:')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static DownloadedDocument downloadDocument(String resourceOrHref, String env, String configuredAccountId, String downloadNamedCredential) {
|
||||||
|
HttpRequest req = new HttpRequest();
|
||||||
|
req.setEndpoint(buildDownloadEndpointForResource(resourceOrHref, configuredAccountId, downloadNamedCredential));
|
||||||
|
req.setMethod('GET');
|
||||||
|
req.setTimeout(HTTP_TIMEOUT);
|
||||||
|
|
||||||
|
HttpResponse res = new Http().send(req);
|
||||||
|
Integer statusCode = res.getStatusCode();
|
||||||
|
if (statusCode < 200 || statusCode >= 300) {
|
||||||
|
throw new CalloutException('CLM download error (HTTP ' + statusCode + '): ' + res.getBody());
|
||||||
|
}
|
||||||
|
|
||||||
|
DownloadedDocument document = new DownloadedDocument();
|
||||||
|
document.body = res.getBodyAsBlob();
|
||||||
|
document.contentType = res.getHeader('Content-Type');
|
||||||
|
document.fileName = extractFileName(res, resourceOrHref, document.contentType);
|
||||||
|
return document;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build the DataXML string from the case payload.
|
* Build the DataXML string from the case payload.
|
||||||
* Flat fields become direct child elements of <TemplateFieldData>.
|
* Flat fields become direct child elements of <TemplateFieldData>.
|
||||||
|
|
@ -115,7 +285,7 @@ public class CLMDocGenCallout {
|
||||||
// Emit flat fields first
|
// Emit flat fields first
|
||||||
for (String key : payload.keySet()) {
|
for (String key : payload.keySet()) {
|
||||||
if (key == 'DeficiencyList') continue;
|
if (key == 'DeficiencyList') continue;
|
||||||
xml += '<' + key + '>' + escapeXml(String.valueOf(payload.get(key))) + '</' + key + '>';
|
xml += '<' + key + '>' + escapeXml(safeValue(payload.get(key))) + '</' + key + '>';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Emit DeficiencyList as a nested list so templates can iterate dynamically
|
// Emit DeficiencyList as a nested list so templates can iterate dynamically
|
||||||
|
|
@ -125,9 +295,10 @@ public class CLMDocGenCallout {
|
||||||
for (Integer i = 0; i < deficiencies.size(); i++) {
|
for (Integer i = 0; i < deficiencies.size(); i++) {
|
||||||
Map<String, Object> d = (Map<String, Object>) deficiencies[i];
|
Map<String, Object> d = (Map<String, Object>) deficiencies[i];
|
||||||
xml += '<Deficiency>';
|
xml += '<Deficiency>';
|
||||||
xml += '<Number>' + escapeXml(String.valueOf(d.get('deficiencyNumber'))) + '</Number>';
|
xml += '<Number>' + escapeXml(safeValue(d.get('deficiencyNumber'))) + '</Number>';
|
||||||
xml += '<Description>' + escapeXml(String.valueOf(d.get('description'))) + '</Description>';
|
xml += '<Description>' + escapeXml(safeValue(d.get('description'))) + '</Description>';
|
||||||
xml += '<Resolution>' + escapeXml(String.valueOf(d.get('resolution'))) + '</Resolution>';
|
xml += '<Resolution>' + escapeXml(safeValue(d.get('resolution'))) + '</Resolution>';
|
||||||
|
xml += '<Reference>' + escapeXml(safeValue(d.get('reference'))) + '</Reference>';
|
||||||
xml += '</Deficiency>';
|
xml += '</Deficiency>';
|
||||||
}
|
}
|
||||||
xml += '</DeficiencyList>';
|
xml += '</DeficiencyList>';
|
||||||
|
|
@ -138,6 +309,93 @@ public class CLMDocGenCallout {
|
||||||
return xml;
|
return xml;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static String prettyPrintXml(String xml) {
|
||||||
|
if (String.isBlank(xml)) {
|
||||||
|
return xml;
|
||||||
|
}
|
||||||
|
|
||||||
|
String normalized = xml
|
||||||
|
.replace('><', '>\n<')
|
||||||
|
.replace('\r\n', '\n')
|
||||||
|
.replace('\r', '\n');
|
||||||
|
|
||||||
|
List<String> lines = normalized.split('\n');
|
||||||
|
List<String> formatted = new List<String>();
|
||||||
|
Integer indent = 0;
|
||||||
|
|
||||||
|
for (String rawLine : lines) {
|
||||||
|
String line = rawLine == null ? '' : rawLine.trim();
|
||||||
|
if (line == '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Boolean isClosing = line.startsWith('</');
|
||||||
|
Boolean isDeclaration = line.startsWith('<?') || line.startsWith('<!');
|
||||||
|
Boolean isSelfClosing = line.endsWith('/>') || (line.contains('</') && line.indexOf('</') > 0);
|
||||||
|
|
||||||
|
if (isClosing && indent > 0) {
|
||||||
|
indent--;
|
||||||
|
}
|
||||||
|
|
||||||
|
formatted.add(repeatIndent(indent) + line);
|
||||||
|
|
||||||
|
if (!isClosing && !isSelfClosing && !isDeclaration) {
|
||||||
|
indent++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return String.join(formatted, '\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Map<String, Object> buildRequestBodyMap(
|
||||||
|
Map<String, Object> casePayload,
|
||||||
|
String templateDocHref,
|
||||||
|
String destinationFolderHref,
|
||||||
|
String destinationDocName
|
||||||
|
) {
|
||||||
|
return new Map<String, Object>{
|
||||||
|
'TemplateDocument' => new Map<String, Object>{
|
||||||
|
'Href' => templateDocHref
|
||||||
|
},
|
||||||
|
'DataXML' => buildDataXml(casePayload),
|
||||||
|
'DestinationDocumentName' => destinationDocName,
|
||||||
|
'DestinationFolder' => new Map<String, Object>{
|
||||||
|
'Href' => destinationFolderHref
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String extractBaseUrl(String resourceOrHref) {
|
||||||
|
if (String.isBlank(resourceOrHref)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (!(resourceOrHref.startsWith('http://') || resourceOrHref.startsWith('https://'))) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Integer schemeIndex = resourceOrHref.indexOf('//');
|
||||||
|
Integer pathStart = resourceOrHref.indexOf('/', schemeIndex + 2);
|
||||||
|
return pathStart > 0 ? resourceOrHref.substring(0, pathStart) : resourceOrHref;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String defaultBaseUrl(String env) {
|
||||||
|
return env == 'S1'
|
||||||
|
? 'https://api.s1.us.clm.demo.docusign.net'
|
||||||
|
: 'https://apiuatna11.springcm.com';
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String repeatIndent(Integer indent) {
|
||||||
|
String value = '';
|
||||||
|
for (Integer i = 0; i < indent; i++) {
|
||||||
|
value += ' ';
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String safeValue(Object val) {
|
||||||
|
return val != null ? String.valueOf(val) : '';
|
||||||
|
}
|
||||||
|
|
||||||
private static String escapeXml(String s) {
|
private static String escapeXml(String s) {
|
||||||
if (s == null) return '';
|
if (s == null) return '';
|
||||||
return s.replace('&', '&')
|
return s.replace('&', '&')
|
||||||
|
|
@ -152,26 +410,160 @@ public class CLMDocGenCallout {
|
||||||
String body = res.getBody();
|
String body = res.getBody();
|
||||||
if (statusCode >= 200 && statusCode < 300) {
|
if (statusCode >= 200 && statusCode < 300) {
|
||||||
Map<String, Object> m = (Map<String, Object>) JSON.deserializeUntyped(body);
|
Map<String, Object> m = (Map<String, Object>) JSON.deserializeUntyped(body);
|
||||||
String href = (String) m.get('Href');
|
String href = firstString(m, new List<String>{ 'Href', 'Uri', 'Location' });
|
||||||
String status = (String) m.get('Status');
|
String status = firstString(m, new List<String>{ 'Status', 'State' });
|
||||||
String taskId = href != null ? href.substringAfterLast('/') : null;
|
String taskId = href != null ? href.substringAfterLast('/') : null;
|
||||||
return new CLMDocGenResponse(true, 'Task status: ' + status, href, taskId);
|
String generatedDocumentUrl = findFirstDocumentHref(m);
|
||||||
|
String generatedDocumentId = generatedDocumentUrl != null ? generatedDocumentUrl.substringAfterLast('/') : null;
|
||||||
|
String message = 'Task status: ' + (String.isNotBlank(status) ? status : 'Unknown');
|
||||||
|
return new CLMDocGenResponse(true, message, href, taskId, status, generatedDocumentUrl, generatedDocumentId, body);
|
||||||
} else {
|
} else {
|
||||||
return new CLMDocGenResponse(false, 'CLM API Error (HTTP ' + statusCode + '): ' + body, null, null);
|
return new CLMDocGenResponse(false, 'CLM API Error (HTTP ' + statusCode + '): ' + body, null, null, null, null, null, body);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static String firstNonBlank(String firstValue, String secondValue, String thirdValue, String fallbackValue) {
|
||||||
|
if (String.isNotBlank(firstValue)) {
|
||||||
|
return firstValue;
|
||||||
|
}
|
||||||
|
if (String.isNotBlank(secondValue)) {
|
||||||
|
return secondValue;
|
||||||
|
}
|
||||||
|
if (String.isNotBlank(thirdValue)) {
|
||||||
|
return thirdValue;
|
||||||
|
}
|
||||||
|
return fallbackValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String firstString(Map<String, Object> source, List<String> keys) {
|
||||||
|
for (String key : keys) {
|
||||||
|
Object value = source.get(key);
|
||||||
|
if (value != null) {
|
||||||
|
String asString = String.valueOf(value);
|
||||||
|
if (String.isNotBlank(asString)) {
|
||||||
|
return asString;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String findFirstDocumentHref(Object node) {
|
||||||
|
if (node instanceof Map<String, Object>) {
|
||||||
|
Map<String, Object> mapNode = (Map<String, Object>) node;
|
||||||
|
String href = firstString(mapNode, new List<String>{ 'Href', 'Uri', 'Location' });
|
||||||
|
if (String.isNotBlank(href) && href.contains('/documents/') && !href.contains('/documentxmlmergetasks/')) {
|
||||||
|
return href;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Object value : mapNode.values()) {
|
||||||
|
String nestedHref = findFirstDocumentHref(value);
|
||||||
|
if (String.isNotBlank(nestedHref)) {
|
||||||
|
return nestedHref;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (node instanceof List<Object>) {
|
||||||
|
for (Object item : (List<Object>) node) {
|
||||||
|
String nestedHref = findFirstDocumentHref(item);
|
||||||
|
if (String.isNotBlank(nestedHref)) {
|
||||||
|
return nestedHref;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String extractFileName(HttpResponse res, String resourceOrHref, String contentType) {
|
||||||
|
String disposition = res.getHeader('Content-Disposition');
|
||||||
|
if (String.isNotBlank(disposition)) {
|
||||||
|
Integer marker = disposition.toLowerCase().indexOf('filename=');
|
||||||
|
if (marker >= 0) {
|
||||||
|
String candidate = disposition.substring(marker + 9).trim();
|
||||||
|
if (candidate.startsWith('"') && candidate.endsWith('"') && candidate.length() >= 2) {
|
||||||
|
candidate = candidate.substring(1, candidate.length() - 1);
|
||||||
|
}
|
||||||
|
if (String.isNotBlank(candidate)) {
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String baseName = String.isNotBlank(resourceOrHref) ? resourceOrHref.substringAfterLast('/') : 'generated-document';
|
||||||
|
String extension = inferExtension(contentType);
|
||||||
|
if (String.isNotBlank(extension) && !baseName.toLowerCase().endsWith(extension)) {
|
||||||
|
return baseName + extension;
|
||||||
|
}
|
||||||
|
return baseName;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String inferExtension(String contentType) {
|
||||||
|
if (String.isBlank(contentType)) {
|
||||||
|
return '.docx';
|
||||||
|
}
|
||||||
|
|
||||||
|
String normalizedType = contentType.toLowerCase();
|
||||||
|
if (normalizedType.contains('pdf')) {
|
||||||
|
return '.pdf';
|
||||||
|
}
|
||||||
|
if (normalizedType.contains('wordprocessingml') || normalizedType.contains('officedocument')) {
|
||||||
|
return '.docx';
|
||||||
|
}
|
||||||
|
if (normalizedType.contains('msword')) {
|
||||||
|
return '.doc';
|
||||||
|
}
|
||||||
|
if (normalizedType.contains('json')) {
|
||||||
|
return '.json';
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
public class CLMDocGenResponse {
|
public class CLMDocGenResponse {
|
||||||
|
@AuraEnabled
|
||||||
public Boolean success;
|
public Boolean success;
|
||||||
|
@AuraEnabled
|
||||||
public String message;
|
public String message;
|
||||||
|
@AuraEnabled
|
||||||
public String documentUrl;
|
public String documentUrl;
|
||||||
|
@AuraEnabled
|
||||||
public String documentId;
|
public String documentId;
|
||||||
|
@AuraEnabled
|
||||||
|
public String taskStatus;
|
||||||
|
@AuraEnabled
|
||||||
|
public String generatedDocumentUrl;
|
||||||
|
@AuraEnabled
|
||||||
|
public String generatedDocumentId;
|
||||||
|
@AuraEnabled
|
||||||
|
public String taskDetailsJson;
|
||||||
|
|
||||||
public CLMDocGenResponse(Boolean success, String message, String documentUrl, String documentId) {
|
public CLMDocGenResponse(Boolean success, String message, String documentUrl, String documentId) {
|
||||||
this.success = success;
|
this(success, message, documentUrl, documentId, null, null, null, null);
|
||||||
this.message = message;
|
}
|
||||||
|
|
||||||
|
public CLMDocGenResponse(
|
||||||
|
Boolean success,
|
||||||
|
String message,
|
||||||
|
String documentUrl,
|
||||||
|
String documentId,
|
||||||
|
String taskStatus,
|
||||||
|
String generatedDocumentUrl,
|
||||||
|
String generatedDocumentId,
|
||||||
|
String taskDetailsJson
|
||||||
|
) {
|
||||||
|
this.success = success;
|
||||||
|
this.message = message;
|
||||||
this.documentUrl = documentUrl;
|
this.documentUrl = documentUrl;
|
||||||
this.documentId = documentId;
|
this.documentId = documentId;
|
||||||
|
this.taskStatus = taskStatus;
|
||||||
|
this.generatedDocumentUrl = generatedDocumentUrl;
|
||||||
|
this.generatedDocumentId = generatedDocumentId;
|
||||||
|
this.taskDetailsJson = taskDetailsJson;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class DownloadedDocument {
|
||||||
|
public Blob body;
|
||||||
|
public String fileName;
|
||||||
|
public String contentType;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,155 @@
|
||||||
|
@IsTest
|
||||||
|
private class CLMDocGenCalloutTest {
|
||||||
|
private class CLMCalloutMock implements HttpCalloutMock {
|
||||||
|
public HttpResponse respond(HttpRequest req) {
|
||||||
|
HttpResponse res = new HttpResponse();
|
||||||
|
res.setHeader('Content-Type', 'application/json');
|
||||||
|
|
||||||
|
if (req.getMethod() == 'POST' && req.getEndpoint().contains('/documentxmlmergetasks')) {
|
||||||
|
System.assert(req.getBody().contains('TemplateDocument'));
|
||||||
|
System.assert(req.getBody().contains('DestinationFolder'));
|
||||||
|
System.assert(req.getBody().contains('DeficiencyList'));
|
||||||
|
System.assert(req.getBody().contains('&'));
|
||||||
|
res.setStatusCode(200);
|
||||||
|
res.setBody('{"Href":"https://uatna11.springcm.com/v2/2371cf36-eb8a-43fe-9f28-b5bbe7644397/documentxmlmergetasks/TASK-123","Status":"Queued"}');
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.getMethod() == 'GET' && req.getEndpoint().contains('/documentxmlmergetasks/TASK-123')) {
|
||||||
|
res.setStatusCode(200);
|
||||||
|
res.setBody('{"Href":"https://uatna11.springcm.com/v2/2371cf36-eb8a-43fe-9f28-b5bbe7644397/documentxmlmergetasks/TASK-123","Status":"Completed"}');
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.getMethod() == 'GET' && req.getEndpoint().contains('/documents/template-guid')) {
|
||||||
|
res.setStatusCode(200);
|
||||||
|
res.setHeader('Content-Type', 'application/pdf');
|
||||||
|
res.setHeader('Content-Disposition', 'attachment; filename="GeneratedReview.pdf"');
|
||||||
|
res.setBodyAsBlob(Blob.valueOf('pdf-bytes'));
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.setStatusCode(404);
|
||||||
|
res.setBody('{"Message":"Not Found"}');
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@IsTest
|
||||||
|
static void generatesDocumentAndPollsTaskStatus() {
|
||||||
|
Appraiser_Case__c appraiserCase = new Appraiser_Case__c(
|
||||||
|
Appraiser_Field_Review_Date__c = Date.newInstance(2026, 4, 2),
|
||||||
|
Letter_Sent_Date__c = Date.newInstance(2026, 4, 9),
|
||||||
|
FHA_Case_Number__c = '123-4567890',
|
||||||
|
Appraiser_Name__c = 'Jamie Appraiser',
|
||||||
|
Appraiser_Last_Name__c = 'Appraiser',
|
||||||
|
Property_Street__c = '123 Main & Main <Suite 5>',
|
||||||
|
Property_City__c = 'Denver',
|
||||||
|
Property_State_Province__c = 'CO',
|
||||||
|
Property_Postal_Code__c = '80202'
|
||||||
|
);
|
||||||
|
insert appraiserCase;
|
||||||
|
|
||||||
|
insert new Appraiser_Case_Deficiency__c(
|
||||||
|
Appraiser_Case__c = appraiserCase.Id,
|
||||||
|
Deficiency_Number__c = 1,
|
||||||
|
Description__c = 'Missing comparable sale adjustment detail.',
|
||||||
|
Resolution__c = 'Added supporting calculations & notes.',
|
||||||
|
Reference__c = 'VC-1'
|
||||||
|
);
|
||||||
|
|
||||||
|
Test.setMock(HttpCalloutMock.class, new CLMCalloutMock());
|
||||||
|
|
||||||
|
Test.startTest();
|
||||||
|
CLMDocGenCallout.CLMDocGenResponse response = CLMDocGenCallout.generateDocument(
|
||||||
|
appraiserCase.Id,
|
||||||
|
'https://uatna11.springcm.com/v2/2371cf36-eb8a-43fe-9f28-b5bbe7644397/documents/template-guid',
|
||||||
|
'https://uatna11.springcm.com/v2/2371cf36-eb8a-43fe-9f28-b5bbe7644397/folders/folder-guid',
|
||||||
|
'Review_AC-00001.docx',
|
||||||
|
'UAT'
|
||||||
|
);
|
||||||
|
CLMDocGenCallout.CLMDocGenResponse taskStatus = CLMDocGenCallout.getTaskStatus('TASK-123', 'S1');
|
||||||
|
Test.stopTest();
|
||||||
|
|
||||||
|
System.assertEquals(true, response.success);
|
||||||
|
System.assertEquals('TASK-123', response.documentId);
|
||||||
|
System.assert(response.documentUrl.contains('/documentxmlmergetasks/TASK-123'));
|
||||||
|
System.assertEquals(true, taskStatus.success);
|
||||||
|
System.assertEquals('Task status: Completed', taskStatus.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
@IsTest
|
||||||
|
static void normalizesResourcePathsAndExtractsAccountIds() {
|
||||||
|
System.assertEquals(
|
||||||
|
'2371cf36-eb8a-43fe-9f28-b5bbe7644397',
|
||||||
|
CLMDocGenCallout.extractAccountId('https://uatna11.springcm.com/v2/2371cf36-eb8a-43fe-9f28-b5bbe7644397/folders/folder-guid')
|
||||||
|
);
|
||||||
|
System.assertEquals(
|
||||||
|
'/v2/2371cf36-eb8a-43fe-9f28-b5bbe7644397/folders/folder-guid',
|
||||||
|
CLMDocGenCallout.normalizeResourcePath(
|
||||||
|
'https://uatna11.springcm.com/v2/2371cf36-eb8a-43fe-9f28-b5bbe7644397/folders/folder-guid',
|
||||||
|
'UAT'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
System.assertEquals(
|
||||||
|
'/v2/' + CLMDocGenCallout.defaultAccountId('UAT') + '/folders/folder-guid',
|
||||||
|
CLMDocGenCallout.normalizeResourcePath('/folders/folder-guid', 'UAT')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@IsTest
|
||||||
|
static void downloadsGeneratedDocumentThroughDownloadCredential() {
|
||||||
|
Test.setMock(HttpCalloutMock.class, new CLMCalloutMock());
|
||||||
|
|
||||||
|
Test.startTest();
|
||||||
|
CLMDocGenCallout.DownloadedDocument downloaded = CLMDocGenCallout.downloadDocument(
|
||||||
|
'https://apiuatna11.springcm.com/v2/bccae332-c7db-4892-ab85-257df0f70fea/documents/template-guid',
|
||||||
|
'UAT'
|
||||||
|
);
|
||||||
|
Test.stopTest();
|
||||||
|
|
||||||
|
System.assertEquals('GeneratedReview.pdf', downloaded.fileName);
|
||||||
|
System.assertEquals('application/pdf', downloaded.contentType);
|
||||||
|
System.assertNotEquals(null, downloaded.body);
|
||||||
|
}
|
||||||
|
|
||||||
|
@IsTest
|
||||||
|
static void buildsPreviewXmlAndRequestBody() {
|
||||||
|
Appraiser_Case__c appraiserCase = new Appraiser_Case__c(
|
||||||
|
Appraiser_Field_Review_Date__c = Date.newInstance(2026, 4, 2),
|
||||||
|
Letter_Sent_Date__c = Date.newInstance(2026, 4, 9),
|
||||||
|
FHA_Case_Number__c = '123-4567890',
|
||||||
|
Appraiser_Name__c = 'Jamie Appraiser',
|
||||||
|
Appraiser_Last_Name__c = 'Appraiser',
|
||||||
|
Property_Street__c = '245 Lexington Ave',
|
||||||
|
Property_City__c = 'New York',
|
||||||
|
Property_State_Province__c = 'NY',
|
||||||
|
Property_Postal_Code__c = '10016',
|
||||||
|
Property_Country__c = 'USA'
|
||||||
|
);
|
||||||
|
insert appraiserCase;
|
||||||
|
|
||||||
|
insert new Appraiser_Case_Deficiency__c(
|
||||||
|
Appraiser_Case__c = appraiserCase.Id,
|
||||||
|
Deficiency_Number__c = 1,
|
||||||
|
Description__c = 'Missing comparable sale adjustment detail.',
|
||||||
|
Resolution__c = 'Added supporting calculations.',
|
||||||
|
Reference__c = 'VC-1'
|
||||||
|
);
|
||||||
|
|
||||||
|
String dataXml = CLMDocGenCallout.buildDataXmlForCase(appraiserCase.Id);
|
||||||
|
String requestBodyJson = CLMDocGenCallout.buildRequestBodyJson(
|
||||||
|
appraiserCase.Id,
|
||||||
|
'https://apiuatna11.springcm.com/v2/bccae332-c7db-4892-ab85-257df0f70fea/documents/template-guid',
|
||||||
|
'https://apiuatna11.springcm.com/v2/bccae332-c7db-4892-ab85-257df0f70fea/folders/folder-guid',
|
||||||
|
'Review_AC-000002.docx'
|
||||||
|
);
|
||||||
|
|
||||||
|
System.assert(dataXml.contains('<LetterSentDate>'));
|
||||||
|
System.assert(dataXml.contains('<FHACaseNumber>123-4567890</FHACaseNumber>'));
|
||||||
|
System.assert(dataXml.contains('<Reference>VC-1</Reference>'));
|
||||||
|
System.assert(requestBodyJson.contains('"DataXML"'));
|
||||||
|
System.assert(requestBodyJson.contains('Review_AC-000002.docx'));
|
||||||
|
System.assert(requestBodyJson.contains('template-guid'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
|
||||||
|
<apiVersion>63.0</apiVersion>
|
||||||
|
<status>Active</status>
|
||||||
|
</ApexClass>
|
||||||
|
|
@ -0,0 +1,339 @@
|
||||||
|
public with sharing class DocusignESignatureService {
|
||||||
|
public class ESignatureAccountConfig {
|
||||||
|
@AuraEnabled public String accountCode;
|
||||||
|
@AuraEnabled public String accountDisplayName;
|
||||||
|
@AuraEnabled public String environment;
|
||||||
|
@AuraEnabled public String eSignatureAuthNamedCredential;
|
||||||
|
@AuraEnabled public String eSignatureRestNamedCredential;
|
||||||
|
@AuraEnabled public String eSignatureAccountId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ApiResponse {
|
||||||
|
@AuraEnabled public Boolean success;
|
||||||
|
@AuraEnabled public Integer statusCode;
|
||||||
|
@AuraEnabled public String message;
|
||||||
|
@AuraEnabled public String requestPath;
|
||||||
|
@AuraEnabled public String responseBody;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ESignatureAccountSummary {
|
||||||
|
@AuraEnabled public String accountId;
|
||||||
|
@AuraEnabled public String accountName;
|
||||||
|
@AuraEnabled public String baseUri;
|
||||||
|
@AuraEnabled public Boolean isDefault;
|
||||||
|
@AuraEnabled public String rawJson;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TemplateSummary {
|
||||||
|
@AuraEnabled public String templateId;
|
||||||
|
@AuraEnabled public String name;
|
||||||
|
@AuraEnabled public String description;
|
||||||
|
@AuraEnabled public String shared;
|
||||||
|
@AuraEnabled public String lastModified;
|
||||||
|
@AuraEnabled public String rawJson;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class EnvelopeSummary {
|
||||||
|
@AuraEnabled public String envelopeId;
|
||||||
|
@AuraEnabled public String emailSubject;
|
||||||
|
@AuraEnabled public String status;
|
||||||
|
@AuraEnabled public String createdDateTime;
|
||||||
|
@AuraEnabled public String sentDateTime;
|
||||||
|
@AuraEnabled public String completedDateTime;
|
||||||
|
@AuraEnabled public String rawJson;
|
||||||
|
}
|
||||||
|
|
||||||
|
@AuraEnabled(cacheable=true)
|
||||||
|
public static ESignatureAccountConfig getAccountConfig(String accountCode) {
|
||||||
|
CLM_Account_Setting__mdt row = requireAccountSetting(accountCode);
|
||||||
|
ESignatureAccountConfig config = new ESignatureAccountConfig();
|
||||||
|
config.accountCode = String.isNotBlank(row.Account_Code__c) ? row.Account_Code__c : row.DeveloperName;
|
||||||
|
config.accountDisplayName = String.isNotBlank(row.Account_Display_Name__c) ? row.Account_Display_Name__c : row.DeveloperName;
|
||||||
|
config.environment = row.Environment_Code__c;
|
||||||
|
config.eSignatureAuthNamedCredential = row.ESignature_Auth_Named_Credential__c;
|
||||||
|
config.eSignatureRestNamedCredential = row.ESignature_Rest_Named_Credential__c;
|
||||||
|
config.eSignatureAccountId = row.ESignature_Account_Id__c;
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
@AuraEnabled(cacheable=false)
|
||||||
|
public static ApiResponse probe(String accountCode, String relativePath) {
|
||||||
|
CLM_Account_Setting__mdt row = requireAccountSetting(accountCode);
|
||||||
|
String normalizedPath = normalizePath(relativePath);
|
||||||
|
|
||||||
|
HttpRequest req = new HttpRequest();
|
||||||
|
req.setEndpoint(buildEndpoint(normalizedPath, row.ESignature_Rest_Named_Credential__c));
|
||||||
|
req.setMethod('GET');
|
||||||
|
req.setTimeout(30000);
|
||||||
|
|
||||||
|
HttpResponse res = new Http().send(req);
|
||||||
|
ApiResponse response = new ApiResponse();
|
||||||
|
response.success = res.getStatusCode() >= 200 && res.getStatusCode() < 300;
|
||||||
|
response.statusCode = res.getStatusCode();
|
||||||
|
response.message = response.success ? 'eSignature request succeeded.' : 'eSignature request failed.';
|
||||||
|
response.requestPath = normalizedPath;
|
||||||
|
response.responseBody = res.getBody();
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
@AuraEnabled(cacheable=false)
|
||||||
|
public static List<ESignatureAccountSummary> listAccounts(String accountCode) {
|
||||||
|
ApiResponse response = getLoginInformation(accountCode);
|
||||||
|
if (!response.success) {
|
||||||
|
throw new AuraHandledException('eSignature API Error (HTTP ' + response.statusCode + '): ' + response.responseBody);
|
||||||
|
}
|
||||||
|
return parseAccountList(response.responseBody);
|
||||||
|
}
|
||||||
|
|
||||||
|
@AuraEnabled(cacheable=false)
|
||||||
|
public static ApiResponse getLoginInformation(String accountCode) {
|
||||||
|
return probe(accountCode, '/v2.1/login_information');
|
||||||
|
}
|
||||||
|
|
||||||
|
@AuraEnabled(cacheable=false)
|
||||||
|
public static ApiResponse getUserInfo(String accountCode) {
|
||||||
|
CLM_Account_Setting__mdt row = requireAccountSetting(accountCode);
|
||||||
|
|
||||||
|
HttpRequest req = new HttpRequest();
|
||||||
|
req.setEndpoint(buildEndpoint('/oauth/userinfo', authNamedCredential(row)));
|
||||||
|
req.setMethod('GET');
|
||||||
|
req.setTimeout(30000);
|
||||||
|
|
||||||
|
HttpResponse res = new Http().send(req);
|
||||||
|
ApiResponse response = new ApiResponse();
|
||||||
|
response.success = res.getStatusCode() >= 200 && res.getStatusCode() < 300;
|
||||||
|
response.statusCode = res.getStatusCode();
|
||||||
|
response.message = response.success ? 'eSignature user info request succeeded.' : 'eSignature user info request failed.';
|
||||||
|
response.requestPath = '/oauth/userinfo';
|
||||||
|
response.responseBody = res.getBody();
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
@AuraEnabled(cacheable=false)
|
||||||
|
public static ApiResponse getAccountInformation(String accountCode, String eSignatureAccountId) {
|
||||||
|
CLM_Account_Setting__mdt row = requireAccountSetting(accountCode);
|
||||||
|
String targetAccountId = requireESignatureAccountId(row, accountCode, eSignatureAccountId);
|
||||||
|
return probe(accountCode, '/v2.1/accounts/' + targetAccountId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@AuraEnabled(cacheable=false)
|
||||||
|
public static List<TemplateSummary> listTemplates(String accountCode) {
|
||||||
|
CLM_Account_Setting__mdt row = requireAccountSetting(accountCode);
|
||||||
|
String targetAccountId = requireESignatureAccountId(row, accountCode, null);
|
||||||
|
ApiResponse response = probe(accountCode, '/v2.1/accounts/' + targetAccountId + '/templates');
|
||||||
|
if (!response.success) {
|
||||||
|
throw new AuraHandledException('eSignature API Error (HTTP ' + response.statusCode + '): ' + response.responseBody);
|
||||||
|
}
|
||||||
|
return parseTemplateList(response.responseBody);
|
||||||
|
}
|
||||||
|
|
||||||
|
@AuraEnabled(cacheable=false)
|
||||||
|
public static List<EnvelopeSummary> listEnvelopes(String accountCode, String fromDate) {
|
||||||
|
CLM_Account_Setting__mdt row = requireAccountSetting(accountCode);
|
||||||
|
String targetAccountId = requireESignatureAccountId(row, accountCode, null);
|
||||||
|
String normalizedFromDate = String.isBlank(fromDate)
|
||||||
|
? DateTime.newInstanceGMT(Date.today().addDays(-30), Time.newInstance(0, 0, 0, 0)).formatGMT('yyyy-MM-dd')
|
||||||
|
: fromDate.trim();
|
||||||
|
ApiResponse response = probe(
|
||||||
|
accountCode,
|
||||||
|
'/v2.1/accounts/' + targetAccountId + '/envelopes?from_date=' + EncodingUtil.urlEncode(normalizedFromDate, 'UTF-8')
|
||||||
|
);
|
||||||
|
if (!response.success) {
|
||||||
|
throw new AuraHandledException('eSignature API Error (HTTP ' + response.statusCode + '): ' + response.responseBody);
|
||||||
|
}
|
||||||
|
return parseEnvelopeList(response.responseBody);
|
||||||
|
}
|
||||||
|
|
||||||
|
@TestVisible
|
||||||
|
private static List<ESignatureAccountSummary> parseAccountList(String body) {
|
||||||
|
Object root = JSON.deserializeUntyped(body);
|
||||||
|
List<Object> records = new List<Object>();
|
||||||
|
if (root instanceof List<Object>) {
|
||||||
|
records = (List<Object>) root;
|
||||||
|
} else if (root instanceof Map<String, Object>) {
|
||||||
|
Object loginAccounts = ((Map<String, Object>) root).get('loginAccounts');
|
||||||
|
if (loginAccounts instanceof List<Object>) {
|
||||||
|
records = (List<Object>) loginAccounts;
|
||||||
|
}
|
||||||
|
Object accounts = ((Map<String, Object>) root).get('accounts');
|
||||||
|
if (records.isEmpty() && accounts instanceof List<Object>) {
|
||||||
|
records = (List<Object>) accounts;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<ESignatureAccountSummary> summaries = new List<ESignatureAccountSummary>();
|
||||||
|
for (Object record : records) {
|
||||||
|
if (!(record instanceof Map<String, Object>)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Map<String, Object> row = (Map<String, Object>) record;
|
||||||
|
ESignatureAccountSummary summary = new ESignatureAccountSummary();
|
||||||
|
summary.accountId = firstString(row, new List<String>{ 'accountId', 'account_id' });
|
||||||
|
summary.accountName = firstString(row, new List<String>{ 'accountName', 'account_name', 'name' });
|
||||||
|
summary.baseUri = firstString(row, new List<String>{ 'baseUri', 'base_uri', 'baseUrl', 'base_url' });
|
||||||
|
summary.isDefault = parseBoolean(row.get('isDefault'));
|
||||||
|
summary.rawJson = JSON.serialize(row);
|
||||||
|
summaries.add(summary);
|
||||||
|
}
|
||||||
|
return summaries;
|
||||||
|
}
|
||||||
|
|
||||||
|
@TestVisible
|
||||||
|
private static List<TemplateSummary> parseTemplateList(String body) {
|
||||||
|
List<Object> records = extractCollection(body, new List<String>{ 'envelopeTemplates', 'templates' });
|
||||||
|
List<TemplateSummary> summaries = new List<TemplateSummary>();
|
||||||
|
for (Object record : records) {
|
||||||
|
if (!(record instanceof Map<String, Object>)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Map<String, Object> row = (Map<String, Object>) record;
|
||||||
|
TemplateSummary summary = new TemplateSummary();
|
||||||
|
summary.templateId = firstString(row, new List<String>{ 'templateId', 'template_id' });
|
||||||
|
summary.name = firstString(row, new List<String>{ 'name' });
|
||||||
|
summary.description = firstString(row, new List<String>{ 'description' });
|
||||||
|
summary.shared = firstString(row, new List<String>{ 'shared' });
|
||||||
|
summary.lastModified = firstString(row, new List<String>{ 'lastModified', 'last_modified', 'lastModifiedDateTime' });
|
||||||
|
summary.rawJson = JSON.serialize(row);
|
||||||
|
summaries.add(summary);
|
||||||
|
}
|
||||||
|
return summaries;
|
||||||
|
}
|
||||||
|
|
||||||
|
@TestVisible
|
||||||
|
private static List<EnvelopeSummary> parseEnvelopeList(String body) {
|
||||||
|
List<Object> records = extractCollection(body, new List<String>{ 'envelopes' });
|
||||||
|
List<EnvelopeSummary> summaries = new List<EnvelopeSummary>();
|
||||||
|
for (Object record : records) {
|
||||||
|
if (!(record instanceof Map<String, Object>)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Map<String, Object> row = (Map<String, Object>) record;
|
||||||
|
EnvelopeSummary summary = new EnvelopeSummary();
|
||||||
|
summary.envelopeId = firstString(row, new List<String>{ 'envelopeId', 'envelope_id' });
|
||||||
|
summary.emailSubject = firstString(row, new List<String>{ 'emailSubject', 'email_subject' });
|
||||||
|
summary.status = firstString(row, new List<String>{ 'status' });
|
||||||
|
summary.createdDateTime = firstString(row, new List<String>{ 'createdDateTime', 'created_datetime' });
|
||||||
|
summary.sentDateTime = firstString(row, new List<String>{ 'sentDateTime', 'sent_datetime' });
|
||||||
|
summary.completedDateTime = firstString(row, new List<String>{ 'completedDateTime', 'completed_datetime' });
|
||||||
|
summary.rawJson = JSON.serialize(row);
|
||||||
|
summaries.add(summary);
|
||||||
|
}
|
||||||
|
return summaries;
|
||||||
|
}
|
||||||
|
|
||||||
|
@TestVisible
|
||||||
|
private static String buildEndpoint(String relativePath, String namedCredential) {
|
||||||
|
if (String.isBlank(namedCredential)) {
|
||||||
|
throw new AuraHandledException('No eSignature named credential is configured for this account.');
|
||||||
|
}
|
||||||
|
return 'callout:' + namedCredential + normalizePath(relativePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String normalizePath(String relativePath) {
|
||||||
|
if (String.isBlank(relativePath)) {
|
||||||
|
throw new AuraHandledException('A relative path is required.');
|
||||||
|
}
|
||||||
|
return relativePath.startsWith('/') ? relativePath : '/' + relativePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static CLM_Account_Setting__mdt requireAccountSetting(String accountCode) {
|
||||||
|
String normalizedCode = String.isBlank(accountCode) ? null : accountCode.trim();
|
||||||
|
List<CLM_Account_Setting__mdt> rows = new List<CLM_Account_Setting__mdt>();
|
||||||
|
if (String.isNotBlank(normalizedCode)) {
|
||||||
|
rows = [
|
||||||
|
SELECT DeveloperName,
|
||||||
|
Account_Code__c,
|
||||||
|
Account_Display_Name__c,
|
||||||
|
Environment_Code__c,
|
||||||
|
ESignature_Auth_Named_Credential__c,
|
||||||
|
ESignature_Rest_Named_Credential__c,
|
||||||
|
ESignature_Account_Id__c,
|
||||||
|
Active__c
|
||||||
|
FROM CLM_Account_Setting__mdt
|
||||||
|
WHERE Active__c = true
|
||||||
|
AND DeveloperName = :normalizedCode
|
||||||
|
LIMIT 1
|
||||||
|
];
|
||||||
|
if (rows.isEmpty()) {
|
||||||
|
rows = [
|
||||||
|
SELECT DeveloperName,
|
||||||
|
Account_Code__c,
|
||||||
|
Account_Display_Name__c,
|
||||||
|
Environment_Code__c,
|
||||||
|
ESignature_Auth_Named_Credential__c,
|
||||||
|
ESignature_Rest_Named_Credential__c,
|
||||||
|
ESignature_Account_Id__c,
|
||||||
|
Active__c
|
||||||
|
FROM CLM_Account_Setting__mdt
|
||||||
|
WHERE Active__c = true
|
||||||
|
AND Account_Code__c = :normalizedCode
|
||||||
|
LIMIT 1
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rows.isEmpty()) {
|
||||||
|
throw new AuraHandledException('No active CLM account setting was found for ' + accountCode + '.');
|
||||||
|
}
|
||||||
|
|
||||||
|
CLM_Account_Setting__mdt row = rows[0];
|
||||||
|
if (String.isBlank(row.ESignature_Rest_Named_Credential__c)) {
|
||||||
|
throw new AuraHandledException('No eSignature named credential is configured for ' + accountCode + '.');
|
||||||
|
}
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String authNamedCredential(CLM_Account_Setting__mdt row) {
|
||||||
|
return String.isNotBlank(row.ESignature_Auth_Named_Credential__c)
|
||||||
|
? row.ESignature_Auth_Named_Credential__c
|
||||||
|
: row.ESignature_Rest_Named_Credential__c;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String requireESignatureAccountId(CLM_Account_Setting__mdt row, String accountCode, String eSignatureAccountId) {
|
||||||
|
String targetAccountId = String.isNotBlank(eSignatureAccountId) ? eSignatureAccountId : row.ESignature_Account_Id__c;
|
||||||
|
if (String.isBlank(targetAccountId)) {
|
||||||
|
throw new AuraHandledException('No eSignature account id is configured for ' + accountCode + '.');
|
||||||
|
}
|
||||||
|
return targetAccountId;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<Object> extractCollection(String body, List<String> keys) {
|
||||||
|
Object root = JSON.deserializeUntyped(body);
|
||||||
|
if (root instanceof List<Object>) {
|
||||||
|
return (List<Object>) root;
|
||||||
|
}
|
||||||
|
if (root instanceof Map<String, Object>) {
|
||||||
|
Map<String, Object> source = (Map<String, Object>) root;
|
||||||
|
for (String key : keys) {
|
||||||
|
Object records = source.get(key);
|
||||||
|
if (records instanceof List<Object>) {
|
||||||
|
return (List<Object>) records;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new List<Object>();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String firstString(Map<String, Object> source, List<String> keys) {
|
||||||
|
for (String key : keys) {
|
||||||
|
Object value = source.get(key);
|
||||||
|
if (value != null) {
|
||||||
|
String text = String.valueOf(value);
|
||||||
|
if (String.isNotBlank(text)) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Boolean parseBoolean(Object value) {
|
||||||
|
if (value == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (value instanceof Boolean) {
|
||||||
|
return (Boolean) value;
|
||||||
|
}
|
||||||
|
return String.valueOf(value).toLowerCase() == 'true';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
|
||||||
|
<apiVersion>63.0</apiVersion>
|
||||||
|
<status>Active</status>
|
||||||
|
</ApexClass>
|
||||||
|
|
@ -0,0 +1,150 @@
|
||||||
|
@IsTest
|
||||||
|
private class DocusignESignatureServiceTest {
|
||||||
|
private class ESignatureMock implements HttpCalloutMock {
|
||||||
|
public HttpResponse respond(HttpRequest req) {
|
||||||
|
HttpResponse res = new HttpResponse();
|
||||||
|
res.setHeader('Content-Type', 'application/json');
|
||||||
|
|
||||||
|
if (req.getMethod() == 'GET' && req.getEndpoint().contains('/oauth/userinfo')) {
|
||||||
|
res.setStatusCode(200);
|
||||||
|
res.setBody('{"sub":"d9aab149-ff54-408c-a748-baa4b56e2fcd","accounts":[{"account_id":"12345678","account_name":"Demo eSignature Account","base_uri":"https://demo.docusign.net","is_default":true}]}');
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.getMethod() == 'GET' && req.getEndpoint().contains('/templates')) {
|
||||||
|
res.setStatusCode(200);
|
||||||
|
res.setBody('{"envelopeTemplates":[{"templateId":"tmpl-001","name":"Review Letter","description":"Appraiser review letter template","shared":"true","lastModified":"2026-04-08T12:00:00Z"},{"templateId":"tmpl-002","name":"Alternate Review Letter","description":"Alternate version","shared":"false","lastModified":"2026-04-07T12:00:00Z"}]}');
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.getMethod() == 'GET' && req.getEndpoint().contains('/envelopes?from_date=')) {
|
||||||
|
res.setStatusCode(200);
|
||||||
|
res.setBody('{"envelopes":[{"envelopeId":"env-001","emailSubject":"Appraiser Review","status":"completed","createdDateTime":"2026-04-01T10:00:00Z","sentDateTime":"2026-04-01T10:05:00Z","completedDateTime":"2026-04-01T10:15:00Z"},{"envelopeId":"env-002","emailSubject":"Second Review","status":"sent","createdDateTime":"2026-04-02T09:00:00Z","sentDateTime":"2026-04-02T09:05:00Z"}]}');
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.getMethod() == 'GET' && req.getEndpoint().contains('/v2.1/accounts/12345678')) {
|
||||||
|
res.setStatusCode(200);
|
||||||
|
res.setBody('{"accountId":"12345678","accountName":"Demo eSignature Account","status":"active"}');
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.getMethod() == 'GET' && req.getEndpoint().contains('/v2.1/login_information')) {
|
||||||
|
res.setStatusCode(200);
|
||||||
|
res.setBody('{"loginAccounts":[{"accountId":"12345678","name":"Demo eSignature Account","baseUrl":"https://demo.docusign.net/restapi/v2.1/accounts/12345678","isDefault":"true"},{"accountId":"87654321","name":"Secondary Demo Account","baseUrl":"https://demo.docusign.net/restapi/v2.1/accounts/87654321","isDefault":"false"}]}');
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.setStatusCode(404);
|
||||||
|
res.setBody('{"message":"Not Found"}');
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@IsTest
|
||||||
|
static void returnsConfiguredAccountInfo() {
|
||||||
|
Test.startTest();
|
||||||
|
DocusignESignatureService.ESignatureAccountConfig config = DocusignESignatureService.getAccountConfig('DTC_CLM_Demo');
|
||||||
|
Test.stopTest();
|
||||||
|
|
||||||
|
System.assertEquals('DTC_CLM_Demo', config.accountCode);
|
||||||
|
System.assertEquals('DTC CLM Demo', config.accountDisplayName);
|
||||||
|
System.assertEquals('AcctDemo_NamedCreds', config.eSignatureAuthNamedCredential);
|
||||||
|
System.assertEquals('Esignature_Demo_NamedCreds', config.eSignatureRestNamedCredential);
|
||||||
|
}
|
||||||
|
|
||||||
|
@IsTest
|
||||||
|
static void listsAccountsFromEsignatureApi() {
|
||||||
|
Test.setMock(HttpCalloutMock.class, new ESignatureMock());
|
||||||
|
|
||||||
|
Test.startTest();
|
||||||
|
List<DocusignESignatureService.ESignatureAccountSummary> accounts = DocusignESignatureService.listAccounts('DTC_CLM_Demo');
|
||||||
|
Test.stopTest();
|
||||||
|
|
||||||
|
System.assertEquals(2, accounts.size());
|
||||||
|
System.assertEquals('12345678', accounts[0].accountId);
|
||||||
|
System.assertEquals('Demo eSignature Account', accounts[0].accountName);
|
||||||
|
System.assertEquals('https://demo.docusign.net/restapi/v2.1/accounts/12345678', accounts[0].baseUri);
|
||||||
|
System.assertEquals(true, accounts[0].isDefault);
|
||||||
|
}
|
||||||
|
|
||||||
|
@IsTest
|
||||||
|
static void getsLoginInformationUsingWorkingEndpoint() {
|
||||||
|
Test.setMock(HttpCalloutMock.class, new ESignatureMock());
|
||||||
|
|
||||||
|
Test.startTest();
|
||||||
|
DocusignESignatureService.ApiResponse response = DocusignESignatureService.getLoginInformation('DTC_CLM_Demo');
|
||||||
|
Test.stopTest();
|
||||||
|
|
||||||
|
System.assertEquals(true, response.success);
|
||||||
|
System.assertEquals(200, response.statusCode);
|
||||||
|
System.assert(response.responseBody.contains('loginAccounts'));
|
||||||
|
}
|
||||||
|
|
||||||
|
@IsTest
|
||||||
|
static void getsUserInfoUsingAuthNamedCredential() {
|
||||||
|
Test.setMock(HttpCalloutMock.class, new ESignatureMock());
|
||||||
|
|
||||||
|
Test.startTest();
|
||||||
|
DocusignESignatureService.ApiResponse response = DocusignESignatureService.getUserInfo('DTC_CLM_Demo');
|
||||||
|
Test.stopTest();
|
||||||
|
|
||||||
|
System.assertEquals(true, response.success);
|
||||||
|
System.assertEquals(200, response.statusCode);
|
||||||
|
System.assertEquals('/oauth/userinfo', response.requestPath);
|
||||||
|
System.assert(response.responseBody.contains('"accounts"'));
|
||||||
|
}
|
||||||
|
|
||||||
|
@IsTest
|
||||||
|
static void getsAccountInformationUsingConfiguredAccountId() {
|
||||||
|
Test.setMock(HttpCalloutMock.class, new ESignatureMock());
|
||||||
|
|
||||||
|
Test.startTest();
|
||||||
|
DocusignESignatureService.ApiResponse response = DocusignESignatureService.getAccountInformation(
|
||||||
|
'DTC_CLM_Demo',
|
||||||
|
'12345678'
|
||||||
|
);
|
||||||
|
Test.stopTest();
|
||||||
|
|
||||||
|
System.assertEquals(true, response.success);
|
||||||
|
System.assertEquals(200, response.statusCode);
|
||||||
|
System.assert(response.responseBody.contains('Demo eSignature Account'));
|
||||||
|
}
|
||||||
|
|
||||||
|
@IsTest
|
||||||
|
static void listsTemplatesUsingConfiguredAccountId() {
|
||||||
|
Test.setMock(HttpCalloutMock.class, new ESignatureMock());
|
||||||
|
|
||||||
|
Test.startTest();
|
||||||
|
List<DocusignESignatureService.TemplateSummary> templates = DocusignESignatureService.listTemplates('DTC_CLM_Demo');
|
||||||
|
Test.stopTest();
|
||||||
|
|
||||||
|
System.assertEquals(2, templates.size());
|
||||||
|
System.assertEquals('tmpl-001', templates[0].templateId);
|
||||||
|
System.assertEquals('Review Letter', templates[0].name);
|
||||||
|
}
|
||||||
|
|
||||||
|
@IsTest
|
||||||
|
static void listsRecentEnvelopesUsingConfiguredAccountId() {
|
||||||
|
Test.setMock(HttpCalloutMock.class, new ESignatureMock());
|
||||||
|
|
||||||
|
Test.startTest();
|
||||||
|
List<DocusignESignatureService.EnvelopeSummary> envelopes = DocusignESignatureService.listEnvelopes(
|
||||||
|
'DTC_CLM_Demo',
|
||||||
|
'2026-04-01'
|
||||||
|
);
|
||||||
|
Test.stopTest();
|
||||||
|
|
||||||
|
System.assertEquals(2, envelopes.size());
|
||||||
|
System.assertEquals('env-001', envelopes[0].envelopeId);
|
||||||
|
System.assertEquals('completed', envelopes[0].status);
|
||||||
|
}
|
||||||
|
|
||||||
|
@IsTest
|
||||||
|
static void buildsEndpointWithNamedCredential() {
|
||||||
|
System.assertEquals(
|
||||||
|
'callout:Esignature_Demo_NamedCreds/v2.1/accounts',
|
||||||
|
DocusignESignatureService.buildEndpoint('/v2.1/accounts', 'Esignature_Demo_NamedCreds')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
|
||||||
|
<apiVersion>63.0</apiVersion>
|
||||||
|
<status>Active</status>
|
||||||
|
</ApexClass>
|
||||||
Loading…
Reference in New Issue