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>();
|
||||
payload.put('AppraiserCaseNumber', appraiserCase.Name);
|
||||
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
|
||||
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('description', deficiency.Description__c);
|
||||
defMap.put('resolution', deficiency.Resolution__c);
|
||||
defMap.put('reference', deficiency.Reference__c);
|
||||
deficiencyList.add(defMap);
|
||||
}
|
||||
}
|
||||
|
|
@ -60,8 +84,23 @@ public class AppraiserCasePayloadBuilder {
|
|||
Id,
|
||||
Name,
|
||||
Appraiser_Field_Review_Date__c,
|
||||
Property_Address__c,
|
||||
(SELECT Id, Deficiency_Number__c, Description__c, Resolution__c
|
||||
Letter_Sent_Date__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
|
||||
ORDER BY Deficiency_Number__c ASC)
|
||||
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
|
||||
* @return String Formatted date or null
|
||||
*/
|
||||
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() {
|
||||
// Create test Appraiser Case
|
||||
Appraiser_Case__c testCase = new Appraiser_Case__c(
|
||||
Appraiser_Field_Review_Date__c = Date.parse('04/02/2026'),
|
||||
Property_Address__c = '123 Main St, Denver, CO 80202'
|
||||
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_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;
|
||||
|
||||
|
|
@ -16,13 +31,15 @@ private class AppraiserCasePayloadBuilderTest {
|
|||
Appraiser_Case__c = testCase.Id,
|
||||
Deficiency_Number__c = 1,
|
||||
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(
|
||||
Appraiser_Case__c = testCase.Id,
|
||||
Deficiency_Number__c = 2,
|
||||
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;
|
||||
}
|
||||
|
|
@ -36,11 +53,39 @@ private class AppraiserCasePayloadBuilderTest {
|
|||
Assert.isNotNull(payload, 'Payload should not be null');
|
||||
Assert.isTrue(payload.containsKey('AppraiserCaseNumber'), 'Payload should contain AppraiserCaseNumber');
|
||||
Assert.isTrue(payload.containsKey('AppraiserFieldReviewDate'), 'Payload should contain AppraiserFieldReviewDate');
|
||||
Assert.isTrue(payload.containsKey('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('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.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');
|
||||
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
|
||||
|
|
@ -58,7 +103,10 @@ private class AppraiserCasePayloadBuilderTest {
|
|||
static void testPayloadWithNullDate() {
|
||||
// Create case without review date
|
||||
Appraiser_Case__c testCase = new Appraiser_Case__c(
|
||||
Property_Address__c = '456 Oak Ave, Boulder, CO 80301'
|
||||
Property_Street__c = '456 Oak Ave',
|
||||
Property_City__c = 'Boulder',
|
||||
Property_State_Province__c = 'CO',
|
||||
Property_Postal_Code__c = '80301'
|
||||
);
|
||||
insert testCase;
|
||||
|
||||
|
|
@ -66,6 +114,7 @@ private class AppraiserCasePayloadBuilderTest {
|
|||
|
||||
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.areEqual('456 Oak Ave, Boulder, CO 80301', (String) payload.get('PropertyAddress'));
|
||||
}
|
||||
|
||||
@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>
|
||||
|
|
@ -2,17 +2,94 @@ public class CLMDocGenCallout {
|
|||
|
||||
// S1 demo environment
|
||||
private static final String CLM_ACCOUNT_ID_S1 = '2371cf36-eb8a-43fe-9f28-b5bbe7644397';
|
||||
private static final String CLM_BASE_S1 = 'callout:CLMNamedCred/v2/' + CLM_ACCOUNT_ID_S1;
|
||||
private static final String CLM_NAMED_CRED_S1 = 'callout:CLMs1NamedCreds';
|
||||
private static final String CLM_DOWNLOAD_NAMED_CRED_S1 = 'callout:CLMs1Download';
|
||||
|
||||
// UAT demo environment
|
||||
private static final String CLM_ACCOUNT_ID_UAT = 'bccae332-c7db-4892-ab85-257df0f70fea';
|
||||
private static final String CLM_BASE_UAT = 'callout:CLMuatNamedCreds/v2/' + CLM_ACCOUNT_ID_UAT;
|
||||
private static final 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'. */
|
||||
private static String clmBase(String env) {
|
||||
return env == 'S1' ? CLM_BASE_S1 : CLM_BASE_UAT;
|
||||
/** Resolve the correct CLM Named Credential base. env: 'UAT' or 'S1'. */
|
||||
private static String clmNamedCredential(String env) {
|
||||
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. */
|
||||
|
|
@ -40,21 +117,42 @@ public class CLMDocGenCallout {
|
|||
String destinationDocName,
|
||||
String env
|
||||
) {
|
||||
Map<String, Object> casePayload = AppraiserCasePayloadBuilder.buildPayload(caseId);
|
||||
|
||||
Map<String, Object> requestBody = new Map<String, Object>{
|
||||
'TemplateDocument' => new Map<String, Object>{
|
||||
'Href' => templateDocHref
|
||||
},
|
||||
'DataXML' => buildDataXml(casePayload),
|
||||
'DestinationDocumentName' => destinationDocName,
|
||||
'DestinationFolder' => new Map<String, Object>{
|
||||
'Href' => destinationFolderHref
|
||||
return generateDocument(
|
||||
caseId,
|
||||
templateDocHref,
|
||||
destinationFolderHref,
|
||||
destinationDocName,
|
||||
env,
|
||||
defaultAccountId(env),
|
||||
clmNamedCredential(env).substringAfter('callout:')
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
public static CLMDocGenResponse generateDocument(
|
||||
String caseId,
|
||||
String templateDocHref,
|
||||
String destinationFolderHref,
|
||||
String destinationDocName,
|
||||
String env,
|
||||
String configuredAccountId,
|
||||
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();
|
||||
req.setEndpoint(clmBase(env) + '/documentxmlmergetasks');
|
||||
req.setEndpoint('callout:' + apiNamedCredential + '/v2/' + accountId + '/documentxmlmergetasks');
|
||||
req.setMethod('POST');
|
||||
req.setHeader('Content-Type', 'application/json');
|
||||
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). */
|
||||
public static CLMDocGenResponse getTaskStatus(String taskId) {
|
||||
return getTaskStatus(taskId, 'UAT');
|
||||
}
|
||||
|
||||
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();
|
||||
req.setEndpoint(clmBase(env) + '/documentxmlmergetasks/' + taskId);
|
||||
req.setEndpoint(buildEndpointForResource('/documentxmlmergetasks/' + taskId, configuredAccountId, apiNamedCredential));
|
||||
req.setMethod('GET');
|
||||
req.setTimeout(HTTP_TIMEOUT);
|
||||
try {
|
||||
|
|
@ -96,13 +238,41 @@ public class CLMDocGenCallout {
|
|||
|
||||
public static String probe(String resource, String env) {
|
||||
HttpRequest req = new HttpRequest();
|
||||
req.setEndpoint(clmBase(env) + '/' + resource);
|
||||
req.setEndpoint(buildEndpointForResource(resource, env));
|
||||
req.setMethod('GET');
|
||||
req.setTimeout(HTTP_TIMEOUT);
|
||||
HttpResponse res = new Http().send(req);
|
||||
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.
|
||||
* Flat fields become direct child elements of <TemplateFieldData>.
|
||||
|
|
@ -115,7 +285,7 @@ public class CLMDocGenCallout {
|
|||
// Emit flat fields first
|
||||
for (String key : payload.keySet()) {
|
||||
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
|
||||
|
|
@ -125,9 +295,10 @@ public class CLMDocGenCallout {
|
|||
for (Integer i = 0; i < deficiencies.size(); i++) {
|
||||
Map<String, Object> d = (Map<String, Object>) deficiencies[i];
|
||||
xml += '<Deficiency>';
|
||||
xml += '<Number>' + escapeXml(String.valueOf(d.get('deficiencyNumber'))) + '</Number>';
|
||||
xml += '<Description>' + escapeXml(String.valueOf(d.get('description'))) + '</Description>';
|
||||
xml += '<Resolution>' + escapeXml(String.valueOf(d.get('resolution'))) + '</Resolution>';
|
||||
xml += '<Number>' + escapeXml(safeValue(d.get('deficiencyNumber'))) + '</Number>';
|
||||
xml += '<Description>' + escapeXml(safeValue(d.get('description'))) + '</Description>';
|
||||
xml += '<Resolution>' + escapeXml(safeValue(d.get('resolution'))) + '</Resolution>';
|
||||
xml += '<Reference>' + escapeXml(safeValue(d.get('reference'))) + '</Reference>';
|
||||
xml += '</Deficiency>';
|
||||
}
|
||||
xml += '</DeficiencyList>';
|
||||
|
|
@ -138,6 +309,93 @@ public class CLMDocGenCallout {
|
|||
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) {
|
||||
if (s == null) return '';
|
||||
return s.replace('&', '&')
|
||||
|
|
@ -152,26 +410,160 @@ public class CLMDocGenCallout {
|
|||
String body = res.getBody();
|
||||
if (statusCode >= 200 && statusCode < 300) {
|
||||
Map<String, Object> m = (Map<String, Object>) JSON.deserializeUntyped(body);
|
||||
String href = (String) m.get('Href');
|
||||
String status = (String) m.get('Status');
|
||||
String href = firstString(m, new List<String>{ 'Href', 'Uri', 'Location' });
|
||||
String status = firstString(m, new List<String>{ 'Status', 'State' });
|
||||
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 {
|
||||
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 {
|
||||
@AuraEnabled
|
||||
public Boolean success;
|
||||
@AuraEnabled
|
||||
public String message;
|
||||
@AuraEnabled
|
||||
public String documentUrl;
|
||||
@AuraEnabled
|
||||
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) {
|
||||
this(success, message, documentUrl, documentId, null, null, null, null);
|
||||
}
|
||||
|
||||
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.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