This commit is contained in:
2026-08-21 16:38:06 +00:00
commit 43d764f9bc
31 changed files with 1911 additions and 0 deletions
@@ -0,0 +1,374 @@
public inherited sharing class CreateCaseAutoResponseRule {
private static final String SENDER_NAME = 'Columbus McKinnon Support';
private static final String SENDER_EMAIL = 'cmcoautoreply@columbusmckinnon.com';
private static final Integer MAX_POLLS = 20;
public class Request {
@InvocableVariable(label='Auto Response Rule Id')
public Id autoResponseRuleId;
@InvocableVariable(label='Auto Response Rule Name')
public String autoResponseRuleName;
@InvocableVariable(label='Sort Order')
public Integer sortOrder;
@InvocableVariable(required=true label='Case Origin')
public String caseOrigin;
@InvocableVariable(required=true label='Email Template Id')
public Id emailTemplateId;
}
public class Result {
@InvocableVariable(label='Async Apex Job Id')
public Id asyncJobId;
@InvocableVariable(label='Status')
public String status;
@InvocableVariable(label='Metadata Full Name')
public String metadataFullName;
}
public class Configuration {
public Id autoResponseRuleId;
public String autoResponseRuleName;
public Integer sortOrder;
public String caseOrigin;
public String templateFullName;
}
@InvocableMethod(
label='Create Case Auto-Response Rule Entry'
description='Queues a file-based Metadata API deployment that appends an entry to a Case auto-response rule.'
category='Queue Configuration'
callout=true
)
public static List<Result> createRuleEntry(List<Request> requests) {
MetadataXml.require(
requests != null && requests.size() == 1,
'Create Case Auto-Response Rule Entry accepts exactly one request per invocation.'
);
Request input = requests[0];
input.autoResponseRuleName = input.autoResponseRuleName == null
? null
: input.autoResponseRuleName.trim();
input.caseOrigin = input.caseOrigin == null
? null
: input.caseOrigin.trim();
MetadataXml.require(
input.autoResponseRuleId != null ||
!String.isBlank(input.autoResponseRuleName),
'Auto Response Rule Id or Auto Response Rule Name is required.'
);
MetadataXml.require(
!String.isBlank(input.caseOrigin),
'Case Origin is required.'
);
MetadataXml.require(
input.emailTemplateId != null,
'Email Template Id is required.'
);
MetadataXml.require(
input.sortOrder == null || input.sortOrder > 0,
'Sort Order must be greater than zero.'
);
List<EmailTemplate> templates = [
SELECT Id, DeveloperName, FolderId
FROM EmailTemplate
WHERE Id = :input.emailTemplateId
WITH USER_MODE
LIMIT 1
];
MetadataXml.require(
!templates.isEmpty(),
'Email Template Id was not found.'
);
List<Folder> folders = [
SELECT Id, DeveloperName
FROM Folder
WHERE Id = :templates[0].FolderId AND Type = 'Email'
WITH USER_MODE
LIMIT 1
];
MetadataXml.require(
!folders.isEmpty(),
'The email template must be stored in a shared Classic Email Template folder.'
);
Configuration configuration = new Configuration();
configuration.autoResponseRuleId = input.autoResponseRuleId;
configuration.autoResponseRuleName = String.isBlank(
input.autoResponseRuleName
)
? MetadataApiClient.toolingAutoResponseRuleName(input.autoResponseRuleId)
: input.autoResponseRuleName;
configuration.sortOrder = input.sortOrder;
configuration.caseOrigin = input.caseOrigin;
configuration.templateFullName =
folders[0].DeveloperName +
'/' +
templates[0].DeveloperName;
Id jobId = System.enqueueJob(new StartRetrieveJob(configuration));
Result result = new Result();
result.asyncJobId = jobId;
result.status = 'Queued';
result.metadataFullName = 'Case.' + configuration.autoResponseRuleName;
return new List<Result>{ result };
}
private class StartRetrieveJob implements Queueable, Database.AllowsCallouts {
private Configuration configuration;
private StartRetrieveJob(Configuration configuration) {
this.configuration = configuration;
}
public void execute(QueueableContext context) {
String retrieveId = MetadataApiClient.startRetrieve(
'Case.' + configuration.autoResponseRuleName,
'AutoResponseRule'
);
System.enqueueJob(new RetrievePollJob(configuration, retrieveId, 0), 1);
}
}
private class RetrievePollJob implements Queueable, Database.AllowsCallouts {
private Configuration configuration;
private String retrieveId;
private Integer pollCount;
private RetrievePollJob(
Configuration configuration,
String retrieveId,
Integer pollCount
) {
this.configuration = configuration;
this.retrieveId = retrieveId;
this.pollCount = pollCount;
}
public void execute(QueueableContext context) {
MetadataApiClient.RetrieveResult retrieveResult = MetadataApiClient.checkRetrieve(
retrieveId
);
if (!retrieveResult.done) {
if (pollCount >= MAX_POLLS) {
throw new MetadataApiClient.MetadataApiException(
'Timed out waiting for the auto-response rule metadata retrieve.'
);
}
System.enqueueJob(
new RetrievePollJob(configuration, retrieveId, pollCount + 1),
1
);
return;
}
if (
retrieveResult.status != 'Succeeded' ||
retrieveResult.zipFile == null
) {
throw new MetadataApiClient.MetadataApiException(
'Auto-response rule retrieve failed: ' +
(String.isBlank(retrieveResult.errorMessage)
? retrieveResult.status
: retrieveResult.errorMessage)
);
}
Blob deploymentZip = buildDeploymentZip(
retrieveResult.zipFile,
configuration
);
String deployId = MetadataApiClient.startDeploy(deploymentZip);
System.enqueueJob(new DeployPollJob(deployId, 0), 1);
}
}
private class DeployPollJob implements Queueable, Database.AllowsCallouts {
private String deployId;
private Integer pollCount;
private DeployPollJob(String deployId, Integer pollCount) {
this.deployId = deployId;
this.pollCount = pollCount;
}
public void execute(QueueableContext context) {
MetadataApiClient.DeployResult deployResult = MetadataApiClient.checkDeploy(
deployId
);
if (!deployResult.done) {
if (pollCount >= MAX_POLLS) {
throw new MetadataApiClient.MetadataApiException(
'Timed out waiting for the auto-response rule metadata deployment.'
);
}
System.enqueueJob(new DeployPollJob(deployId, pollCount + 1), 1);
return;
}
if (!deployResult.success) {
throw new MetadataApiClient.MetadataApiException(
'Auto-response rule deployment failed: ' +
(String.isBlank(deployResult.errorMessage)
? deployResult.status
: deployResult.errorMessage)
);
}
}
}
@TestVisible
private static Blob buildDeploymentZip(
Blob retrievedZip,
Configuration configuration
) {
Compression.ZipReader reader = new Compression.ZipReader(retrievedZip);
Compression.ZipEntry ruleEntry;
for (String entryName : reader.getEntriesMap().keySet()) {
if (entryName.endsWith('Case.autoResponseRules')) {
ruleEntry = reader.getEntriesMap().get(entryName);
break;
}
}
MetadataXml.require(
ruleEntry != null,
'The retrieved Case auto-response rule file is missing.'
);
String sourceXml = reader.extractEntry(ruleEntry).toString();
String updatedXml = appendRuleEntry(sourceXml, configuration);
String packageXml =
'<?xml version="1.0" encoding="UTF-8"?>' +
'<Package xmlns="http://soap.sforce.com/2006/04/metadata">' +
'<types><members>Case.' +
MetadataXml.escape(configuration.autoResponseRuleName) +
'</members><name>AutoResponseRule</name></types>' +
'<version>' +
MetadataApiClient.API_VERSION +
'</version></Package>';
Compression.ZipWriter writer = new Compression.ZipWriter();
writer.addEntry('package.xml', Blob.valueOf(packageXml));
writer.addEntry(
'autoResponseRules/Case.autoResponseRules',
Blob.valueOf(updatedXml)
);
return writer.getArchive();
}
@TestVisible
private static String appendRuleEntry(
String sourceXml,
Configuration configuration
) {
Dom.Document document = new Dom.Document();
document.load(sourceXml);
Dom.XmlNode root = document.getRootElement();
Dom.XmlNode targetRule;
for (Dom.XmlNode rule : MetadataXml.children(root, 'autoResponseRule')) {
if (
configuration.autoResponseRuleName == MetadataXml.text(rule, 'fullName')
) {
targetRule = rule;
break;
}
}
MetadataXml.require(
targetRule != null,
'Auto-response rule "' +
configuration.autoResponseRuleName +
'" was not found.'
);
List<Dom.XmlNode> existingEntries = MetadataXml.children(
targetRule,
'ruleEntry'
);
for (Dom.XmlNode existingEntry : existingEntries) {
if (
configuration.caseOrigin ==
criterionValue(existingEntry, 'Case.Origin') &&
configuration.templateFullName ==
MetadataXml.text(existingEntry, 'template')
) {
return sourceXml;
}
}
Integer insertionPosition = configuration.sortOrder == null
? existingEntries.size() + 1
: Math.min(configuration.sortOrder, existingEntries.size() + 1);
String newEntry =
'<ruleEntry><booleanFilter>1 AND 2 AND 3 AND 4</booleanFilter>' +
criterion('Case.Origin', 'equals', configuration.caseOrigin) +
criterion('Case.ContactEmail', 'notEqual', '') +
criterion('Case.Status', 'equals', 'Active') +
criterion('Case.RecordTypeId', 'equals', 'Customer Service') +
'<senderEmail>' +
MetadataXml.escape(SENDER_EMAIL) +
'</senderEmail><senderName>' +
MetadataXml.escape(SENDER_NAME) +
'</senderName><template>' +
MetadataXml.escape(configuration.templateFullName) +
'</template></ruleEntry>';
String targetRuleXml = '<autoResponseRule>';
Integer currentEntryPosition = 0;
Boolean inserted = false;
for (Dom.XmlNode child : targetRule.getChildElements()) {
if (child.getName() == 'ruleEntry') {
currentEntryPosition++;
if (!inserted && currentEntryPosition == insertionPosition) {
targetRuleXml += newEntry;
inserted = true;
}
}
targetRuleXml += MetadataXml.serialize(child);
}
if (!inserted) {
targetRuleXml += newEntry;
}
targetRuleXml += '</autoResponseRule>';
String output =
'<?xml version="1.0" encoding="UTF-8"?>' +
'<AutoResponseRules xmlns="http://soap.sforce.com/2006/04/metadata">';
for (Dom.XmlNode rule : root.getChildElements()) {
output += rule == targetRule
? targetRuleXml
: MetadataXml.serialize(rule);
}
return output + '</AutoResponseRules>';
}
private static String criterion(
String field,
String operation,
String value
) {
return '<criteriaItems><field>' +
MetadataXml.escape(field) +
'</field><operation>' +
MetadataXml.escape(operation) +
'</operation><value>' +
MetadataXml.escape(value) +
'</value></criteriaItems>';
}
private static String criterionValue(Dom.XmlNode ruleEntry, String field) {
for (
Dom.XmlNode criterionNode : MetadataXml.children(
ruleEntry,
'criteriaItems'
)
) {
if (field == MetadataXml.text(criterionNode, 'field')) {
return MetadataXml.text(criterionNode, 'value');
}
}
return null;
}
}