Skip to content

Instantly share code, notes, and snippets.

@dhoechst
Last active January 30, 2023 21:13
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save dhoechst/4d065c3e61e38cf9c019d8fb59915232 to your computer and use it in GitHub Desktop.
Save dhoechst/4d065c3e61e38cf9c019d8fb59915232 to your computer and use it in GitHub Desktop.
Salesforce Opportunity Territory Assignment Logic
/*** Apex version of the default logic.
* If opportunity's assigned account is assigned to
* Case 1: 0 territories in active model
* then set territory2Id = null
* Case 2: 1 territory in active model
* then set territory2Id = account's territory2Id
* Case 3: 2 or more territories in active model
* then set territory2Id = account's territory2Id that is of highest priority.
* But if multiple territories have same highest priority, then set territory2Id = null
*/
global class OppTerrAssignDefaultLogicFilter implements TerritoryMgmt.OpportunityTerritory2AssignmentFilter {
/**
* No-arg constructor.
*/
global OppTerrAssignDefaultLogicFilter() {}
/**
* Get mapping of opportunity to territory2Id. The incoming list of opportunityIds contains only those with IsExcludedFromTerritory2Filter=false.
* If territory2Id = null in result map, clear the opportunity.territory2Id if set.
* If opportunity is not present in result map, its territory2Id remains intact.
*/
global Map<Id,Id> getOpportunityTerritory2Assignments(List<Id> opportunityIds) {
Map<Id, Id> OppIdTerritoryIdResult = new Map<Id, Id>();
if(activeModelId != null){
List<Opportunity> opportunities =
[Select Id, AccountId, Territory2Id from Opportunity where Id IN :opportunityIds];
Set<Id> accountIds = new Set<Id>();
// Create set of parent accountIds
for(Opportunity opp:opportunities){
if(opp.AccountId != null){
accountIds.add(opp.AccountId);
}
}
Map<Id,Territory2Priority> accountMaxPriorityTerritory = getAccountMaxPriorityTerritory(activeModelId, accountIds);
// For each opportunity, assign the highest priority territory if there is no conflict, else assign null.
for(Opportunity opp: opportunities){
Territory2Priority tp = accountMaxPriorityTerritory.get(opp.AccountId);
// Assign highest priority territory if there is only 1.
if((tp != null) && (tp.moreTerritoriesAtPriority == false) && (tp.territory2Id != opp.Territory2Id)){
OppIdTerritoryIdResult.put(opp.Id, tp.territory2Id);
}else{
OppIdTerritoryIdResult.put(opp.Id, null);
}
}
}
return OppIdTerritoryIdResult;
}
/**
* Query assigned territoryIds in active model for given accountIds.
* Create a map of accountId to max priority territory.
*/
private Map<Id,Territory2Priority> getAccountMaxPriorityTerritory(Id activeModelId, Set<Id> accountIds){
Map<Id,Territory2Priority> accountMaxPriorityTerritory = new Map<Id,Territory2Priority>();
for(ObjectTerritory2Association ota:[Select ObjectId, Territory2Id, Territory2.Territory2Type.Priority from ObjectTerritory2Association where objectId IN :accountIds and Territory2.Territory2ModelId = :activeModelId]){
Territory2Priority tp = accountMaxPriorityTerritory.get(ota.ObjectId);
if((tp == null) || (ota.Territory2.Territory2Type.Priority > tp.priority)){
// If this is the first territory examined for account or it has greater priority than current highest priority territory, then set this as new highest priority territory.
tp = new Territory2Priority(ota.Territory2Id,ota.Territory2.Territory2Type.priority,false);
}else if(ota.Territory2.Territory2Type.priority == tp.priority){
// The priority of current highest territory is same as this, so set moreTerritoriesAtPriority to indicate multiple highest priority territories seen so far.
tp.moreTerritoriesAtPriority = true;
}
accountMaxPriorityTerritory.put(ota.ObjectId, tp);
}
return accountMaxPriorityTerritory;
}
/**
* Get the Id of the Active Territory Model.
* If none exists, return null.
*/
@testvisible
private Id ActiveModelId {
get {
if (ActiveModelId == null) {
List<Territory2Model> models = [Select Id from Territory2Model where State = 'Active'];
if(models.size() == 1){
activeModelId = models.get(0).Id;
}
}
return activeModelId;
}
private set;
}
/**
* Helper class to help capture territory2Id, its priority, and whether there are more territories with same priority assigned to the account.
*/
private class Territory2Priority {
public Id territory2Id { get; set; }
public Integer priority { get; set; }
public Boolean moreTerritoriesAtPriority { get; set; }
Territory2Priority(Id territory2Id, Integer priority, Boolean moreTerritoriesAtPriority){
this.territory2Id = territory2Id;
this.priority = priority;
this.moreTerritoriesAtPriority = moreTerritoriesAtPriority;
}
}
}
@isTest
private class OppTerrAssignDefaultLogicFilterTest {
@isTest
private static void testActiveModel() {
// This method is just for code coverage. You can't activate a territory model from code.
OppTerrAssignDefaultLogicFilter filter = new OppTerrAssignDefaultLogicFilter();
Id modelId = filter.ActiveModelId;
}
@isTest
private static void testOppTerritory() {
Territory2 terr = new Territory2();
Territory2 terr2 = new Territory2();
OppTerrAssignDefaultLogicFilter filter = new OppTerrAssignDefaultLogicFilter();
System.runAs(new User(Id = UserInfo.getUserId())) {
Territory2Model tm = new Territory2Model(Name = 'test', DeveloperName ='test');
insert tm;
filter.ActiveModelId = tm.Id; //set the active model Id since it can't be queried
Territory2Type tt = [Select Id from Territory2Type limit 1];
terr = new Territory2(Name = 'Test Territory', DeveloperName = 'TestTerritory', Territory2ModelId = tm.Id, Territory2TypeId = tt.Id);
insert terr;
terr2 = new Territory2(Name = 'Test Territory2', DeveloperName = 'TestTerritory2', Territory2ModelId = tm.Id, Territory2TypeId = tt.Id);
insert terr2;
}
// Create a test account. Add any other required fields here
Account a = new Account(Name = 'Test Account');
insert a;
ObjectTerritory2Association ota = new ObjectTerritory2Association(AssociationCause = 'Territory2Manual', ObjectId = a.Id, Territory2Id = terr.Id);
insert ota;
// Create a test opportunity. Add any other required fields here
Opportunity opp = new Opportunity(AccountId = a.Id, Name = 'Test Opportunity');
insert opp
Map<Id, Id> resultMap = filter.getOpportunityTerritory2Assignments(new List<Id>{opp.Id});
System.assertEquals(terr.Id, resultMap.get(opp.Id));
ObjectTerritory2Association ota2 = new ObjectTerritory2Association(AssociationCause = 'Territory2Manual', ObjectId = a.Id, Territory2Id = terr2.Id);
insert ota2;
resultMap = filter.getOpportunityTerritory2Assignments(new List<Id>{opp.Id});
System.assertEquals(null, resultMap.get(opp.Id), 'No territory should be assinged as 2 have the same priority');
}
}
@varcrysis
Copy link

Hello Daniel, I am trying to use your test class but getting the error on line 32 and 37 that Variable does not exist: TestFactory can you please correct that or point me in the right direction? thank you.

@varcrysis
Copy link

Hello Daniel, NVM I am able to resolve this, thank you for your test class :)

@dbetlow
Copy link

dbetlow commented Feb 23, 2021

varcrysis - What did you do to resolve the error around TestFactory?

@dhoechst
Copy link
Author

@dbetlow
Copy link

dbetlow commented Feb 23, 2021

Thanks Daniel. I installed the Test Factory, which resolved the error, but I still get an error trying to deploy to production.

Your code coverage is 68%. You need at least 75% coverage to complete this deployment.

OppTerrAssignDefaultLogicFilterTest | testOppTerritory | System.QueryException: List has no rows for assignment to SObjectStack Trace: Class.OppTerrAssignDefaultLogicFilterTest.testOppTerritory: line 23, column 1

@dhoechst
Copy link
Author

Sounds like you don't have any territory types set up yet?

@dbetlow
Copy link

dbetlow commented Feb 23, 2021

Ugh... Thanks, that was it!

@Rnatik
Copy link

Rnatik commented Nov 25, 2022

@dhoechst Do I understand correctly that this is the Apex Class and Test code that can be simply copy & pasted in another org?

I'm trying to wrap my head around why Salesforce doesn't include this by default and whether we would need custom code.

@dhoechst
Copy link
Author

@Rnatik yes, you should be able to use this mostly as is. You'll need to put this code in a sandbox first, test, and then deploy to production. You might need to update the test class if you have required fields on things like account or opportunity.

By allowing you to write your own logic, you can customize it. This implementation assigns the opportunity to the territory assigned to the account with the highest priority. If two territories on the account have the same level of priority, the opportunity won't be assigned a territory.

@Rnatik
Copy link

Rnatik commented Nov 30, 2022

Thank you so much! I was able to tweak the code and pass coverage. However, when I run the assignment filter, I receive an email that it failed. It worked for a few hundred records. I opened a case with security and also googled the error but couldn't find any resolution. Do you have any insight to this?

Error simply says: Your request to run opportunity territory assignment filter for X has failed. Please try again.

@dhoechst
Copy link
Author

dhoechst commented Dec 1, 2022

I have seen that same error and I'm not really sure what causes it. I could sometimes get that error to go away if I added filters to only have the assignment on smaller groups of opportunities. Opening a case with Salesforce would be my first stop.

@Rnatik
Copy link

Rnatik commented Dec 1, 2022 via email

@Rnatik
Copy link

Rnatik commented Dec 8, 2022

@dhoechst , Our account records have a custom category picklist field. If I want to update the Apex class to only include Opps from Accounts within specific categories, where would I do that in the code? (Think of it as using the standard "Type" field to include/exclude records).

@dhoechst
Copy link
Author

dhoechst commented Dec 9, 2022

I think you could just change the SOQL query on line 27 to filter for only opps that meet that criteria.

@harrisonhoran
Copy link

I am replying here so that other people can be helped by what I have found. I did indeed open a case with support and it was due to validation rules. I was able to tweak the org and I'm running the code again to see if it will now run successfully.

On Thu, Dec 1, 2022 at 11:54 AM Daniel Hoechst @.> wrote: @.* commented on this gist. ------------------------------ I have seen that same error and I'm not really sure what causes it. I could sometimes get that error to go away if I added filters to only have the assignment on smaller groups of opportunities. Opening a case with Salesforce would be my first stop. — Reply to this email directly, view it on GitHub https://gist.github.com/4d065c3e61e38cf9c019d8fb59915232#gistcomment-4387673 or unsubscribe https://github.com/notifications/unsubscribe-auth/ADFUYPJDRRTRBNB5TAXYMZLWLDJ4ZBFKMF2HI4TJMJ2XIZLTSKBKK5TBNR2WLJDHNFZXJJDOMFWWLK3UNBZGKYLEL52HS4DFQKSXMYLMOVS2I5DSOVS2I3TBNVS3W5DIOJSWCZC7OBQXE5DJMNUXAYLOORPWCY3UNF3GS5DZVRZXKYTKMVRXIX3UPFYGLK2HNFZXIQ3PNVWWK3TUUZ2G64DJMNZZDAVEOR4XAZNEM5UXG5FFOZQWY5LFVEYTANRTGE3TENZZU52HE2LHM5SXFJTDOJSWC5DF . You are receiving this email because you commented on the thread. Triage notifications on the go with GitHub Mobile for iOS https://apps.apple.com/app/apple-store/id1477376905?ct=notification-email&mt=8&pt=524675 or Android https://play.google.com/store/apps/details?id=com.github.android&referrer=utm_campaign%3Dnotification-email%26utm_medium%3Demail%26utm_source%3Dgithub .

Thanks for this thread, all. @Rnatik, I suspect I'm getting this error because of validation rules, as well. When you say you were able to 'tweak the org' and I assume get it to work? Mind explaining what that means? Does that mean that you deleted those validation rules? Or do you only run this class periodically and disable the validation rules when you do?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment