salesforce-appraiser-review.../force-app/main/default/classes/CLMDocGenCallout.cls

570 lines
21 KiB
OpenEdge ABL

public class CLMDocGenCallout {
// S1 demo environment
private static final String CLM_ACCOUNT_ID_S1 = '2371cf36-eb8a-43fe-9f28-b5bbe7644397';
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_NAMED_CRED_UAT = 'callout:CLMuatNamedCreds';
private static final String CLM_DOWNLOAD_NAMED_CRED_UAT = 'callout:CLMuatDownload';
public static final Integer HTTP_TIMEOUT = 30000;
/** 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. */
public static CLMDocGenResponse generateDocument(
String caseId,
String templateDocHref,
String destinationFolderHref,
String destinationDocName
) {
return generateDocument(caseId, templateDocHref, destinationFolderHref, destinationDocName, 'UAT');
}
/**
* Generate a merged document via CLM documentxmlmergetasks (no user interaction).
* @param caseId Appraiser_Case__c record Id
* @param templateDocHref Full CLM Href URL of the template .docx document
* @param destinationFolderHref Full CLM Href URL of the destination folder
* @param destinationDocName Filename for the generated document, e.g. "Review_AC-00001.docx"
* @param env 'UAT' (apiuatna11.springcm.com) or 'S1' (api.s1.us.clm.demo.docusign.net)
*/
public static CLMDocGenResponse generateDocument(
String caseId,
String templateDocHref,
String destinationFolderHref,
String destinationDocName,
String env
) {
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('callout:' + apiNamedCredential + '/v2/' + accountId + '/documentxmlmergetasks');
req.setMethod('POST');
req.setHeader('Content-Type', 'application/json');
req.setTimeout(HTTP_TIMEOUT);
req.setBody(JSON.serialize(requestBody));
try {
Http http = new Http();
HttpResponse res = http.send(req);
return parseTaskResponse(res);
} catch (Exception ex) {
return new CLMDocGenResponse(false, 'HTTP Callout Error: ' + ex.getMessage(), null, null);
}
}
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(buildEndpointForResource('/documentxmlmergetasks/' + taskId, configuredAccountId, apiNamedCredential));
req.setMethod('GET');
req.setTimeout(HTTP_TIMEOUT);
try {
Http http = new Http();
HttpResponse res = http.send(req);
return parseTaskResponse(res);
} catch (Exception ex) {
return new CLMDocGenResponse(false, 'HTTP Callout Error: ' + ex.getMessage(), null, null);
}
}
/** Probe any CLM resource path for debugging. env: 'UAT' or 'S1'. */
public static String probe(String resource) {
return probe(resource, 'UAT');
}
public static String probe(String resource, String env) {
HttpRequest req = new HttpRequest();
req.setEndpoint(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>.
* DeficiencyList items expand into numbered elements:
* Deficiency_1_Number, Deficiency_1_Description, Deficiency_1_Resolution, etc.
*/
private static String buildDataXml(Map<String, Object> payload) {
String xml = '<TemplateFieldData>';
// Emit flat fields first
for (String key : payload.keySet()) {
if (key == 'DeficiencyList') continue;
xml += '<' + key + '>' + escapeXml(safeValue(payload.get(key))) + '</' + key + '>';
}
// Emit DeficiencyList as a nested list so templates can iterate dynamically
List<Object> deficiencies = (List<Object>) payload.get('DeficiencyList');
if (deficiencies != null && !deficiencies.isEmpty()) {
xml += '<DeficiencyList>';
for (Integer i = 0; i < deficiencies.size(); i++) {
Map<String, Object> d = (Map<String, Object>) deficiencies[i];
xml += '<Deficiency>';
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>';
xml += '<DeficiencyCount>' + deficiencies.size() + '</DeficiencyCount>';
}
xml += '</TemplateFieldData>';
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('&', '&amp;')
.replace('<', '&lt;')
.replace('>', '&gt;')
.replace('"', '&quot;')
.replace('\'', '&apos;');
}
private static CLMDocGenResponse parseTaskResponse(HttpResponse res) {
Integer statusCode = res.getStatusCode();
String body = res.getBody();
if (statusCode >= 200 && statusCode < 300) {
Map<String, Object> m = (Map<String, Object>) JSON.deserializeUntyped(body);
String href = firstString(m, new List<String>{ 'Href', 'Uri', 'Location' });
String status = firstString(m, new List<String>{ 'Status', 'State' });
String taskId = href != null ? href.substringAfterLast('/') : null;
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, 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;
}
}