You need to sign in to do that
Don't have an account?
RedmossSubC4i
Bulkification Issue with Apex Program for Opportunity Tracking
I've written an apex program to capture opportunity metrics via timestamps. The goal is to calculate how long it takes a sales rep to reach certain opportunity amount thresholds and how long it takes them to get their first win and first lost opportunity. The amount thresholds are cumulative across multiple opportunities owned by each rep. The program seems to be working when activating the opportunity trigger for one opportunity/owner at a time, but jumbles the data when mass updating many opportunities owned by many users. All of the results are being store in a custom object called Sales Rep Ramp.
Any help at least identifying what the issue is will be greatly appreciated. Still working on it, but it may be during all the sorting, the nested loops, or the static collections in the class or something else. Thank you in advance!
Apex class code is below:
Any help at least identifying what the issue is will be greatly appreciated. Still working on it, but it may be during all the sorting, the nested loops, or the static collections in the class or something else. Thank you in advance!
Apex class code is below:
public class SalesRepRamp { //Recursive Control - Before Trigger public static boolean hasAlreadyRunBefore = false; public static boolean hasAlreadyRunMethodBefore(){ return hasAlreadyRunBefore; } public static void setAlreadyRunMethodBefore(){ hasAlreadyRunBefore = true; } //Recursive Control - After Trigger public static boolean hasAlreadyRunAfter = false; public static boolean hasAlreadyRunMethodAfter(){ return hasAlreadyRunAfter; } public static void setAlreadyRunMethodAfter(){ hasAlreadyRunAfter = true; } //Reference static Set<Id> userIds = new Set<Id>(); static Map<Id,User> userMap = new Map<Id,User>(OpportunitySharedResources.userMap); static Map<Id,Opportunity> oppMap = new Map<Id,Opportunity>(); static Map<String,Sales_Rep_Ramp__c> salesRampMap = new Map<String,Sales_Rep_Ramp__c>(); //Map<UserId+SalesRepRampType,Sales_Rep_Ramp__c> //First Win or First Loss static Map<Id,Map<Date,Opportunity>> userFirstCloseOppMap = new Map<Id,Map<Date,Opportunity>>(); //Map<UserId,Map<CloseDate,Opportunity>> static Map<Id,List<Date>> userFirstCloseDateMap = new Map<Id,List<Date>>(); //Map<UserId,List<CloseDate>> //Pipeline Tracking static Map<Id,Map<Datetime,Opportunity>> userPipelineOppMap = new Map<Id,Map<Datetime,Opportunity>>(); //Map<UserId,Map<Last_Software_Appliance_Modified_Date__c, Opportunity>> static Map<Id,List<Datetime>> userPipelineDatetimeMap = new Map<Id,List<Datetime>>(); //Map<UserId,List<Last_Software_Appliance_Modified_Date__c>> //Results static Map<String,Datetime> thresholdMap = new Map<String,Datetime>(); static Map<Id,Map<String,Datetime>> userResultsMap = new Map<Id,Map<String,Datetime>>(); //Map<UserId,Map<SalesRepRampType,Last_Software_Appliance_Modified_Date__c>> static List<Sales_Rep_Ramp__c> insertSRR = new List<Sales_Rep_Ramp__c>(); public static void entryMethod(List<Opportunity> newList, Map<Id,Opportunity> oldMap, String triggerType){ if(triggerType == 'Before Insert' || triggerType == 'Before Update'){ lastAmountTimestamp(newlist, oldMap, triggerType); adjustClosedLostDate(newList, oldMap, triggerType); } if(triggerType == 'After Insert' || triggerType == 'After Update'){ //Only process Opportunities owned by new sales reps for(Opportunity o: newList){ if(userMap.containsKey(o.OwnerId) && userMap.get(o.OwnerId).Sales_Rep_Ramp__c == true && userMap.get(o.OwnerId).Created_Date__c != null){ userIds.add(o.OwnerId); } } } if(userIds.size() > 0){ getSalesRepRamp(); getRelatedOpps(); processRelatedOpps(newList,oldMap); if(userResultsMap.size() > 0){ updateSalesRepRamp(); } } } public static void lastAmountTimestamp(List<Opportunity> newList, Map<Id,Opportunity> oldMap, String triggerType){ if(triggerType == 'Before Update'){ for(Opportunity o: newList){ Opportunity oldOpp = oldMap.get(o.Id); if(o.Total_Software_and_Appliance__c != oldOpp.Total_Software_and_Appliance__c){ o.Last_Software_Appliance_Modified_Date__c = datetime.now(); } } } } public static void adjustClosedLostDate(List<Opportunity> newList, Map<Id,Opportunity> oldMap, String triggerType){ if(triggerType == 'Before Update'){ for(Opportunity o:newList){ Opportunity oldOpp = oldMap.get(o.Id); if((o.StageName == 'Closed Lost' || o.StageName == 'Closed - Deferred') && oldOpp.StageName != o.StageName && oldOpp.StageName != 'Closed Lost' && oldOpp.StageName != 'Closed - Deferred'){ o.CloseDate = date.today(); } } } } public static void getSalesRepRamp(){ List<Sales_Rep_Ramp__c> salesRampList = new List<Sales_Rep_Ramp__c>([SELECT Id, User__c, Type__c, Timestamp__c FROM Sales_Rep_Ramp__c WHERE User__c IN: userIds]); for(Sales_Rep_Ramp__c srr: salesRampList){ String uniqueKey = srr.User__c+srr.Type__c; salesRampMap.put(uniqueKey,srr); } } public static void getRelatedOpps(){ List<Opportunity> oppList = new List<Opportunity>([ SELECT Id, Name, OwnerId, CloseDate, CreatedDate, StageName, Total_Software_and_Appliance__c, Last_Software_Appliance_Modified_Date__c, Total_Amount__c, IsClosed, IsWon FROM Opportunity WHERE OwnerId IN: userIds AND Total_Software_and_Appliance__c > 0 AND Last_Software_Appliance_Modified_Date__c != null ]); oppMap.putAll(oppList); } public static void processRelatedOpps(List<Opportunity> newList, Map<Id,Opportunity> oldMap){ //Reset thresholdMap thresholdMap.clear(); for(Opportunity o: oppMap.values()){ /*First Win or First Loss - Setup*/ if(o.IsClosed == true){ //Collect Opp by Rep and Date (Close Date) if(userFirstCloseOppMap.containsKey(o.OwnerId)){ userFirstCloseOppMap.get(o.OwnerId).put(o.CloseDate,o); } else{ Map<Date,Opportunity> OppDateMap = new Map<Date,Opportunity>(); OppDateMap.put(o.CloseDate,o); userFirstCloseOppMap.put(o.OwnerId,OppDateMap); } //Collect opps by Rep into Date lists if(userFirstCloseDateMap.containsKey(o.OwnerId)){ userFirstCloseDateMap.get(o.OwnerId).add(o.CloseDate); } else{ List<Date> OppDateList = new List<Date>(); OppDateList.add(o.CloseDate); userFirstCloseDateMap.put(o.OwnerId,OppDateList); } } /*Pipeline - Setup*/ if(o.IsClosed == false || (o.IsClosed == true && o.IsWon == true)){ //Collect Opps by Rep and Datetime (Last Software Appliance Modified Date) if(userPipelineOppMap.containsKey(o.OwnerId)){ userPipelineOppMap.get(o.OwnerId).put(o.Last_Software_Appliance_Modified_Date__c,o); } else{ Map<Datetime,Opportunity> OppDatetimeMap = new Map<Datetime,Opportunity>(); OppDatetimeMap.put(o.Last_Software_Appliance_Modified_Date__c,o); userPipelineOppMap.put(o.OwnerId,OppDatetimeMap); } //Collect Opps by Rep into Datetime lists if(userPipelineDatetimeMap.containsKey(o.OwnerId)){ userPipelineDatetimeMap.get(o.OwnerId).add(o.Last_Software_Appliance_Modified_Date__c); } else{ List<Datetime> OppDatetimeList = new List<Datetime>(); OppDatetimeList.add(o.Last_Software_Appliance_Modified_Date__c); userPipelineDatetimeMap.put(o.OwnerId,OppDatetimeList); } } } /*First Win or First Loss - Process*/ for(Id repId: userFirstCloseDateMap.keySet()){ List<Date> OppDateList = userFirstCloseDateMap.get(repId); //Sort Close Date by oldest first OppDateList.sort(); //Find Opps with First Win or First Loss for(Date closeDate: OppDateList){ Opportunity o = userFirstCloseOppMap.get(repId).get(closeDate); //First Win if(o.IsClosed == true && o.IsWon == true && !thresholdMap.containsKey('First Win')){ Time myTime = Time.newInstance(5,0,0,0); Datetime timestamp = Datetime.newInstance(closeDate,myTime); setupUserResultsMap(repId, 'First Win', timestamp); } //First Loss if(o.IsClosed == true && o.IsWon == false && !thresholdMap.containsKey('First Loss')){ Time myTime = Time.newInstance(5,0,0,0); Datetime timestamp = Datetime.newInstance(closeDate,myTime); setupUserResultsMap(repId, 'First Loss', timestamp); } } } /*Pipeline - Process*/ for(Id repId: userPipelineDatetimeMap.keySet()){ List<Datetime> OppDatetimeList = userPipelineDatetimeMap.get(repId); //Sort Last Software/Appliance Modified Date by oldest first OppDatetimeList.sort(); //SUM Opp Total Software and Appliance in order from oldest to newest Decimal totalSWandApp = 0; for(Datetime timestamp: OppDatetimeList){ Opportunity o = userPipelineOppMap.get(repId).get(timestamp); if(totalSWandApp > 0){ totalSWandApp = totalSWandApp + o.Total_Software_and_Appliance__c; } else{ totalSWandApp = o.Total_Software_and_Appliance__c; } //Check current total and verify Amount thresholds //50k Pipeline if(totalSWandApp >= 50000 && !thresholdMap.containsKey('50k Pipeline')){ setupUserResultsMap(repId, '50k Pipeline', timestamp); } //100k Pipeline if(totalSWandApp >= 100000 && !thresholdMap.containsKey('100k Pipeline')){ setupUserResultsMap(repId, '100k Pipeline', timestamp); } //150k Pipeline if(totalSWandApp >= 150000 && !thresholdMap.containsKey('150k Pipeline')){ setupUserResultsMap(repId, '150k Pipeline', timestamp); } //200k Pipeline if(totalSWandApp >= 200000 && !thresholdMap.containsKey('200k Pipeline')){ setupUserResultsMap(repId, '200k Pipeline', timestamp); } //250k Pipeline if(totalSWandApp >= 250000 && !thresholdMap.containsKey('250k Pipeline')){ setupUserResultsMap(repId, '250k Pipeline', timestamp); } //400k Pipeline if(totalSWandApp >= 400000 && !thresholdMap.containsKey('400k Pipeline')){ setupUserResultsMap(repId, '400k Pipeline', timestamp); } //500k Pipeline if(totalSWandApp >= 500000 && !thresholdMap.containsKey('500k Pipeline')){ setupUserResultsMap(repId, '500k Pipeline', timestamp); } } } } public static void setupUserResultsMap(Id repId, String Type, Datetime timestamp){ if(userResultsMap.containsKey(repId) && !userResultsMap.get(repId).containsKey(Type)){ userResultsMap.get(repId).put(Type,timestamp); } else{ thresholdMap.put(Type,timestamp); userResultsMap.put(repId,thresholdMap); } } public static Sales_Rep_Ramp__c setupSalesRepRamp(Id repId, String Type){ Sales_Rep_Ramp__c srr = new Sales_Rep_Ramp__c( User__c = repId, Type__c = Type, Timestamp__c = userResultsMap.get(repId).get(Type) ); return srr; } public static void processSalesRepRamp(Id userId, String Type){ String uniqueKey = userId+Type; //Check for existing and only insert new Sales Rep Ramp record if(!salesRampMap.containsKey(uniqueKey) && userResultsMap.containsKey(userId) && userResultsMap.get(userId).containsKey(Type)){ Sales_Rep_Ramp__c srr = setupSalesRepRamp(userId, Type); insertSRR.add(srr); } } public static void updateSalesRepRamp(){ //Refresh insert list insertSRR.clear(); //Setup for each threshold for(Id repId: userResultsMap.keySet()){ //50k Pipeline processSalesRepRamp(repId,'50k Pipeline'); //100k Pipeline processSalesRepRamp(repId,'100k Pipeline'); //150k Pipeline processSalesRepRamp(repId,'150k Pipeline'); //200k Pipeline processSalesRepRamp(repId,'200k Pipeline'); //250k Pipeline processSalesRepRamp(repId,'250k Pipeline'); //400k Pipeline processSalesRepRamp(repId,'400k Pipeline'); //500k Pipeline processSalesRepRamp(repId,'500k Pipeline'); //First Win processSalesRepRamp(repId,'First Win'); //First Loss processSalesRepRamp(repId,'First Loss'); } if(insertSRR.size() > 0){ insert insertSRR; } } }
RedmossSubC4i
I think the thresholdMap needs to be reset via thresholdMap.clear() at the start of the setupUserResultsMap method. This is probably what's causing the data to get mixed up. Also, the line to add records to the insertSRR list needs to be inside the for loop otherwise this will only create one new record when processing many reps. Just an initial theory so far, still need to test and verify.