function readOnly(count){ }
Starting November 20, the site will be set to read-only. On December 4, 2023,
forum discussions will move to the Trailblazer Community.
+ Start a Discussion
RedmossSubC4iRedmossSubC4i 

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:
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;
		}
	}
}

 
RedmossSubC4iRedmossSubC4i
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.