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 createRuleEntry(List 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 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 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 }; } 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 = '' + '' + 'Case.' + MetadataXml.escape(configuration.autoResponseRuleName) + 'AutoResponseRule' + '' + MetadataApiClient.API_VERSION + ''; 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 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 = '1 AND 2 AND 3 AND 4' + criterion('Case.Origin', 'equals', configuration.caseOrigin) + criterion('Case.ContactEmail', 'notEqual', '') + criterion('Case.Status', 'equals', 'Active') + criterion('Case.RecordTypeId', 'equals', 'Customer Service') + '' + MetadataXml.escape(SENDER_EMAIL) + '' + MetadataXml.escape(SENDER_NAME) + ''; String targetRuleXml = ''; 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 += ''; String output = '' + ''; for (Dom.XmlNode rule : root.getChildElements()) { output += rule == targetRule ? targetRuleXml : MetadataXml.serialize(rule); } return output + ''; } private static String criterion( String field, String operation, String value ) { return '' + MetadataXml.escape(field) + '' + MetadataXml.escape(operation) + '' + MetadataXml.escape(value) + ''; } 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; } }