0

I am trying to trigger territory assignment rules on accounts via the API (the only way you can, apparently), and then change the account owner based on the territories that are assigned. It’s an interesting process - a managed app first makes an API call in a future method, which updates the account, which triggers my class, which makes another queueable API call to run the assignment rules, which ideally I can then use to update the account owner.

The problem is that my API call apparently updates the account when it runs the assignment rules, which recursively hits the account trigger, which makes the API call, which hits the account trigger, etc. I am able to stop recursion in future or queueable contexts, but how can I stop the API from firing the trigger again?

Note 1: there are no errors given when the code runs. It just fires infinitely.

Note 2: I cannot assign any static variable, because the API transaction is occurring in its own context. For the same reason, !System.isQueueable is not preventing the recursion either.

Trigger looks like this:

if (Trigger.isUpdate) {
    AssignAcctOwnerFromTerritory.AssignOwnerFromTerritory(trigger.new);  //must run in after trigger so that it can grab the territory ID assigned by ETM assignment rules        
}

Class that calls the queueable callout:

public class AssignAcctOwnerFromTerritory {

    public static void AssignOwnerFromTerritory(List<Account> AccsToUpdate){

        //run ETM assignment rules
        Map<ID, Account> AcctMap = new Map<ID, Account>(AccsToUpdate);
        Set<String> AccIDs = new Set<String>((List<String>) New List<ID>(AcctMap.keyset()));


        if(!System.isQueueable()){              
            RunTerritoryRules Job = new RunTerritoryRules();
                Job.AcctIDs = AccIDs;                
                    System.debug('Queuing ETM assignments');
                System.enqueueJob(job);
        }       
}   

The callout:

public class RunTerritoryRules implements Queueable, Database.AllowsCallouts  {

    public Set<String> AcctIDs = null;
    public String SessionID = null;

    public void execute(QueueableContext context) {

        System.debug('AcctIDs queued for ETM rules API call  = '+AcctIDs);      

        SessionID = Page.TerritoriesSessionID.getContent().toString();     
        List<String> AccIDStrings = new List<String>();
        if(AcctIDs != null){
            for(String AccID : AcctIDs){
                AccIDStrings.add(AccountTag.replace('{ACCID}', AccID)); 
            }
        }

        requestTemplate = requestTemplate.replace('{ACCLISTS}', String.join(AccIDStrings, ' '));
        requestTemplate = requestTemplate.replace('{SESSID}', SessionID); 

        HttpRequest request = new HttpRequest();
            request.setEndpoint(System.URL.getSalesforceBaseUrl().toExternalForm()+'/services/Soap/u/41.0/'+UserInfo.getOrganizationId());
            request.setMethod('POST');
            request.setHeader('Content-Type', 'text/xml;charset=UTF-8');
            request.setHeader('SOAPAction', '""');
            request.setBody(requestTemplate);

        String s = String.valueOf(new Http().send(request).getBodyDocument());
            System.debug(s);
        }

        String accountTag = '<urn:sObjects> '+
                            '<urn1:type>Account</urn1:type>  '+
                    '<urn1:Id>{ACCID}</urn1:Id>   '+
                        '</urn:sObjects> ' ;    
        String requestTemplate = '<soapenv:Envelope '+
                'xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"'+
                ' xmlns:urn="urn:partner.soap.sforce.com"'+
                ' xmlns:urn1="urn:sobject.partner.soap.sforce.com">'+
                                '<soapenv:Header> '+
                                  '<urn:AssignmentRuleHeader> '+ 
                                     '<urn:useDefaultRule>true</urn:useDefaultRule> '+
                                     '<urn:assignmentRuleId></urn:assignmentRuleId> '+
                                  '</urn:AssignmentRuleHeader>  '+
                                  '<urn:SessionHeader> '+
                        '<urn:sessionId>{SESSID}</urn:sessionId> '+
                                  '</urn:SessionHeader> '+
                               '</soapenv:Header> '+
                               '<soapenv:Body> '+
                      '<urn:update> '+
                       ' {ACCLISTS}'+ 
                                  '</urn:update> '+
                               '</soapenv:Body> '+
                            '</soapenv:Envelope>';

Relatedly, in the logs, the API call appears as a different transaction from the queueable handler in which it is called, which is different from what I’ve seen making API calls to external web services. Why is that? If the territories will not update within the same transaction as the queueable class in which it is called, I don’t know how I could make the final account owner update. In other integrations I’ve written, the result of the API call (a JSON string for example) is returned within the context of the running class, so it can be used for processing thereafter. 

enter image description here

Many thanks 

1

There are several ways to unravel trigger recursion.
Seeing as in your case, one of the steps is a managed app that you cannot touch I suggest the following:
Make sure that your trigger fires only when a relevant field is modified.

You don't need your trigger to fire every time someone changes a phone number (or whatever field is being changed by the managed app) on an account, only when the territory changes.

Here's a simple sample function of how to get only records that have been modified in a particular field(s).

private static List<SObject> findRelevantModifiedRecords(List<SObject> newList, Map<Id, SObject> oldMap, List<String> relevantFields){
    Set<SObject> retSet = new Set<SObject>();
    SObject tempObj;
    for (SObject obj : newList){
        tempObj = oldMap != null ? oldMap.get(obj.Id) : null;
        for (String field : relevantFields){
            if ((tempObj == null && obj.get(field) != null)  || (tempObj != null && obj.get(field) != tempObj.get(field))){
                retSet.add(obj);
            }
        }
    }
    return new List<SObject>(retSet);
}

This code checks for a created record that has a value in the specified field(s), or a record that received a new value on specified field(s), even if the old value was null.

| improve this answer | |
  • Thanks, good thoughts, but unfortunately the managed app actually is changing the geography related fields that determine the territory. And then the API transaction is happening in its own context, and doesn’t change any fields, it only changes related territories, so I can’t think of anything I could specify in the trigger to indicate that it was triggered in the API context. It’s like a perfect storm of nebulous separate transactions. – number41 Feb 11 at 19:08
  • And of course I need the API transaction to fire the trigger so that I can update the account owner. I just don’t know what I could use to indicate that the trigger was fired from the API context, because the SOAP call does not make any changes to the account. It’s strange that it fires Account triggers at all really. – number41 Feb 11 at 19:19
  • Throw some System.debug()s in there and try to find out what made the Account triggers fire. Salesforce debug logs aren't the most readable things in the world, but you should be able to trace the call stack... – AvailableName Feb 12 at 8:37
0

I solved for this by specifying the exact criteria under which I wanted to make the callout, then using the separate API transaction to trigger the updates I wanted to make after the rules were run. Fortunately the API callout acts as an update the account object even though it is only changing the territory records.

public static void QueueTerritoryAssignment(List<Account> AccsToUpdate, Map<Id, Account> IdToOldAccount){

        Set<String> AccIDs = new Set<String>();

        for (Account Acc : AccsToUpdate){

            if(Trigger.isupdate){                                           
                Account OldAcc = IDToOldAccount.get(Acc.Id);

                if (!Acc.IsExcludedFromRealign && OldAcc.IsExcludedFromRealign ||      
                    (Acc.Company_HQ_State__c != OldAcc.Company_HQ_State__c ||
                     Acc.Company_HQ_Country_Code__c != OldAcc.Company_HQ_Country_Code__c ||
                     Acc.AnnualRevenue != OldAcc.AnnualRevenue ||
                     Acc.Estimated_Annual_Revenue__c != OldAcc.Estimated_Annual_Revenue__c) 
                    ){                            
                        System.debug('AssignOwnerFromTerritory.QueueTerritoryAssignment - Preparing to run assignment rules on account: ' + Acc.Name);                
                    AccIDs.add(Acc.ID);
                }
            }            
        }

        if( !AccIDs.isempty() ) {

            RunTerritoryRules Job = new RunTerritoryRules();
                Job.AcctIDs = AccIDs;               
                    System.debug('AssignOwnerFromTerritory.QueueTerritoryAssignment - Making call to queueable territory assignment rule utility');
                    System.enqueueJob(job);           
        }           
| improve this answer | |

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.