• Maggie Farley 20
  • NEWBIE
  • 10 Points
  • Member since 2017

  • Chatter
    Feed
  • 0
    Best Answers
  • 0
    Likes Received
  • 0
    Likes Given
  • 2
    Questions
  • 1
    Replies
Hello,

Can someone help point out where the NullPointerException occurs? This class is invocable from a process that gets the Opportunity Line Item ID for created and updated records where Product End Date = null. 
 
public with sharing class invocable_productEndDate 
{
  
  /*Params to be passed from different process builder processes from varying objects. 
  None required, because each process will execute slightly different logic and use different params.*/
  public class productVariables 
  {
    @InvocableVariable
    public Id opptyLineItemId;
  }

  @InvocableMethod
  public static void productEndDate(List<productVariables>  Vars)
  {
    List<Id> oppLineItemIds = new List<Id>();
    List<OpportunityLineItem> oppLineItems = new List<OpportunityLineItem>();
    Map<Id, OpportunityLineItem> oliIdandOLI = new Map<Id, OpportunityLineItem>();
    Map<Id, Date> oliAndProductEndDate = new Map<Id, Date>();
    Set<Id> updateOLIIds = new Set<Id>();
    List<OpportunityLineItem> olisToUpdate = new List<OpportunityLineItem>();


    /*Loop through variable objects passed from process builder and assign Ids to lists. Because of SOQL
    governors, don't want to query inside loop, so list assignment works around that*/
    for (productVariables var : Vars)
    {
      if (var.opptyLineItemId != null)
      {
        oppLineItemIds.add(var.opptyLineItemId);
        system.debug('oppLineItemIds = ' + oppLineItemIds);
      }
    }

    oppLineItems = [SELECT Id, ServiceDate, Product_End_Date__c, PricebookEntry.Product2.FY16_Revenue_Type__c
          FROM OpportunityLineItem 
          WHERE Id IN : oppLineItemIds
          AND PricebookEntry.Product2.FY16_Revenue_Type__c = 'Recurring'];

    system.debug('oppLineItems = ' + oppLineItems);

    /*Product Term Months may be null, because Product End Date is not required on Salesforce ui when adding a product. Assume 12 months from ServiceDate and calculate months between.
    Default Product Date based on these assumptions - updates will re-calculate accordingly.*/
    for (OpportunityLineItem oli : oppLineItems)
    {
      oliIdandOLI.put(oli.Id, oli);
      Date tempProductEndDate = oli.ServiceDate.addYears(1).addDays(-1);
      oliAndProductEndDate.put(oli.Id, tempProductEndDate);
    }

    /*Line Item field values to update go in maps in this block*/
    if (!oliIdandOLI.isEmpty())
    {
      for (Id oliID : oliIdandOLI.keySet())
      {
        if (!oliAndProductEndDate.isEmpty())
        {
          oliIdandOLI.get(oliId).Product_End_Date__c = oliAndProductEndDate.get(oliId);
        }
      }
    }

    /*Don't want duplicates in list of line items to update, so loop through ordered set of Ids and add to update list only if the record is not already there.*/
    for (Id oliId : oliIdandOLI.keySet())
    {
      if (!updateOLIIds.contains(oliId))
      {
        olisToUpdate.add(oliIdandOLI.get(oliId));
        system.debug('olisToUpdate =  ' + olisToUpdate);
      }
      else
      {
        updateOLIIds.add(oliId);
        system.debug('updateOLIIds = ' + updateOLIIds);
      }
    }



Thank you,

Maggie
Hi there,

I have been going in circles with an issue on the Case object. The main issue is that Case fields are being updated but the values are immediately deleted.  I am new to Apex and need some help breaking this issue down because I cannot find the root cause. I'd like to simplify the solution, but I'd like to understand what is going on before I make any changes.

Here is an example of the issue:
User-added image


What is supposed to happen:
Whenever a task is completed, a trigger fires to count the number of completed tasks and add that value into the Completed Activities field. When the Completed Activities field is populated, a workflow rule fires to update the FPR field. There is another trigger that updates the Time to FPR(Business Hours) using the FPR field value. I am new to Apex and need some help breaking this issue down because I cannot find the root cause. 

Components:

1. CaseHelper Class
public static void FPRCalc(List<Case> cases, List<Case> oldList, Map<ID, sObject> newMap, Map<ID, sObject> oldMap){
		for(Case c : cases){
			if(c.FPR__c != NULL && c.BusinessHoursId !=NULL){
				if(c.Time_to_FPR_Business_Hours__c != ((Double)(BusinessHours.diff(c.BusinessHoursId, c.CreatedDate, c.FPR__c))/3600000)){
                	c.Time_to_FPR_Business_Hours__c = ((Double)(BusinessHours.diff(c.BusinessHoursId, c.CreatedDate, c.FPR__c))/3600000);
				}
            }

            if(c.Date_Time_Assigned__c != NULL && c.BusinessHoursId !=NULL){
				if(c.Time_to_Assignment_Hours__c != ((Double)(BusinessHours.diff(c.BusinessHoursId, c.CreatedDate, c.Date_Time_Assigned__c))/3600000)){
                	c.Time_to_Assignment_Hours__c = (((Double)(BusinessHours.diff(c.BusinessHoursId, c.CreatedDate, c.Date_Time_Assigned__c))/3600000));
	            }
			}
		}
	}
2. taskAndActivity Trigger
trigger taskAndActivityTrigger on Task (after insert, after update, before delete) 
{

/*	taskandactivity trigger to update Completed Activities
*/
	List<Task> taskList = new List<Task>();
	Set<Id> casesToDecrement = new Set<Id>();
	List<Id> leadIds = new List<Id>();
	Map<Id, Lead> leadIdandLead = new Map<Id, Lead>();
	Map<Id, Decimal> leadIdandCount = new Map<Id, Decimal>();
	Map<Id, Date> leadIdandMaxDate = new Map<Id, Date>();
	Map<Id, Id> leadIdandTaskId = new Map<Id, Id>();
	List<Lead> leadsToUpdate = new List<Lead>();
	Set<Id> leadsToUpdateIds = new Set<Id>();
	Map<Id, Case> caseIdandCase = new Map<Id, Case>();
	List<Id> caseIds = new List<Id>();
    Map<Id, Decimal> caseIdandCount = new Map<Id, Decimal>();
    Map<Id, Datetime> caseIdandDateTime = new Map<Id, DateTime>();
    List<Case> casesToUpdate = new List<Case>();
    Set<Id> casesToUpdateIds = new Set<Id>();

	/*Can't traverse What and Who relationships in Trigger.new, so must query for these fields. Can't use trigger.newMap in delete trigger*/
	if (!Trigger.isDelete)
	{
		taskList = [SELECT Id, Status, isClosed, What.Type, Who.Type, What.Id, Who.Id, ActivityDate, CreatedDate FROM Task Where Id IN : trigger.newMap.keyset() AND Lead_Source_Activity__c != TRUE];
		system.debug('taskList =  ' + taskList);
	}
	else
	{
		taskList = [SELECT Id, Status, isClosed, What.Type, Who.Type, What.Id, Who.Id, ActivityDate, CreatedDate FROM Task Where Id IN : trigger.oldMap.keyset() AND Lead_Source_Activity__c != TRUE];
		system.debug('taskList =  ' + taskList);
	}

	if (!taskList.isEmpty())
	{
		for (Task t : taskList)
		{

			/*Only want closed tasks/activities*/
			if (Trigger.isInsert && t.isClosed)
			{
				system.debug('Trigger.isInsert');

				if (t.What.Type == 'Case')
				{
					system.debug('What.Type = Case');

					/*Can't add to caseIdandCase map here - can't get full Case object off Task. Will query for this and add to map outside of this loop.*/ 
					caseIds.add(t.WhatId);
					system.debug('caseIds = ' + caseIds);
				}


			/*Only want to get the tasks on update that have changed Status*/
			if (Trigger.isUpdate) 
			{

				if ((Trigger.oldMap.get(t.Id).Status != Trigger.newMap.get(t.Id).Status) || (t.Status ==  'Completed' && Trigger.oldMap.get(t.Id).ActivityDate != Trigger.newMap.get(t.Id).ActivityDate))
				{
					system.debug('Trigger.isUpdate and isClosed changed');
					system.debug('new task status = ' + Trigger.newMap.get(t.Id).Status);
					system.debug('old task status = ' + Trigger.oldMap.get(t.Id).Status);

					if (t.What.Type == 'Case')
					{
						system.debug('What.Type = Case');
						/*Can't add to caseIdandCase map here - can't get full Case object off Task. Will query for this and add to map outside of this loop.*/ 
						caseIds.add(t.WhatId);
						system.debug('caseIds = ' + caseIds);
					}

					if (t.Who.Type == 'Lead')
					{
						system.debug('What.Type = Lead');
						/*Can't add to leadIdandLead map here - can't get full Lead object off Task. Will query for this and add to map outside of this loop.*/  
						leadIds.add(t.WhoId);
						system.debug('leadIds = ' + leadIds);
					}
				}
			}

			if (Trigger.isDelete)
			{
				if (t.What.Type == 'Case' && t.isClosed == TRUE)
				{
					leadIds.add(t.WhoId);
					system.debug('leadIds = ' + leadIds);
					system.debug('What.Type = Case');
					casesToDecrement.add(t.WhatId);
					system.debug('casesToDecrement = ' + casesToDecrement);
				}
				if (t.Who.Type == 'Lead' && t.isClosed == TRUE)
				{
					leadIds.add(t.WhoId);
					system.debug('leadIds = ' + leadIds);
					system.debug('Who.Type = Lead');
					leadIdandTaskId.put(t.Id, t.WhoId);
					system.debug('leadIdandTaskId = ' + leadIdandTaskId);
				}
			}
		}
	}

	/*Build key-value pairs from list of ids obtained in trigger context, since task --> case relationship can't be traversed*/ 
	if (!caseIds.isEmpty())
	{
		for (Case c : [SELECT Id, Last_Activity__c, Completed_Activities__c FROM Case WHERE Id IN : caseIds])
		{
			caseIdandCase.put(c.Id, c);
		}
	}

    if (!caseIdandCase.isEmpty())
    {
	    AggregateResult[] caseAggregate = [SELECT WhatId, Count(Id)closedTaskCount FROM Task WHERE isCLosed = TRUE AND What.Type = 'Case' AND WhatId IN : caseIdandCase.keyset() GROUP BY WhatId];
	    system.debug('caseAggregate =  ' + caseAggregate);

	    if (!caseAggregate.isEmpty())
	    {
	    	for (AggregateResult ca : caseAggregate)
	    	{
	    		caseIdandCount.put((ID)ca.get('WhatId'), (Decimal)ca.get('closedTaskCount'));
	    		system.debug('caseIdandCount = ' + caseIdandCount);
	    	}

	    	if (!caseIdandCount.isEmpty())
	    	{
	    		for (Case c : [SELECT Id, Completed_Activities__c FROM CAse WHERE Id IN : caseIdandCount.keyset()])
	    		{
	    			caseIdandCase.get(c.Id).Completed_Activities__c = caseIdandCount.get(c.Id);
	    		}
	    	}
	    }

		/*If task is updated from isClosed to !isClosed, and it is the only task related to that case, we need to decrement it. This list also contains deleted deleted case tasks.*/
		else if (caseAggregate.isEmpty())
		{
			for (Id caseId : caseIdandCase.keySet())
			{
				casesToDecrement.add(caseId);
				system.debug('casesToDecrement = ' + casesToDecrement);
			}
		}

		if (!caseIdandDateTime.isEmpty())
		{
			for (Case c : [SELECT Id, Last_Activity__c, Next_Business_Day_After_Last_Activity__c, BusinessHoursId FROM Case Where Id IN : caseIdandDateTime.keySet()])
			{
				caseIdandCase.get(c.Id).Last_Activity__c = caseIdandDateTime.get(c.Id);
				system.debug('caseIdandCase.get(c.Id).Last_Activity__c ' + caseIdandCase.get(c.Id).Last_Activity__c);
				dateTime tempDateTime = caseIdandDateTime.get(c.Id);
				system.debug('tempDateTime = ' + tempDateTime);

				if (c.BusinessHoursId != null && tempDateTime != null)
				{
					BusinessDays bizDays = new BusinessDays(c.BusinessHoursId);
					system.debug('bizDays = ' + bizDays);
					caseIdandCase.get(c.Id).Next_Business_Day_After_Last_Activity__c = BizDays.nextBusinessDay(date.newInstance(tempDateTime.year(), tempDateTime.month(), tempDateTime.day()));
					system.debug('caseIdandCase.get(c.Id).Next_Business_Day_After_Last_Activity__c = ' + caseIdandCase.get(c.Id).Next_Business_Day_After_Last_Activity__c);
				}
			}
		}

		if (!casesToDecrement.isEmpty())
		{
			for (Case c : [SELECT Id, Completed_Activities__c FROM Case WHERE Id IN : casesToDecrement])
			{
				caseIdandCase.get(c.Id).Completed_Activities__c = caseIdandCase.get(c.Id).Completed_Activities__c - 1;
			}
		}
	}

	/*Loop through the key-value pairs and add them to the list of Cases to pass into the saveResult - only List of sObjects accepted, and don't want dups, hence the set.*/
    for (Id caseId : caseIdAndCase.keySet())
    {
        if (!casesToUpdateIds.contains(caseId))
        {
            casesToUpdate.add(caseIdAndCase.get(caseId));
            system.debug('casesToUpdate = ' + casesToUpdate);
        }
        else
        {
            casesToUpdateIds.add(caseId);
            system.debug('casesToUpdateIds = ' + casesToUpdateIds);
        }
    }

	if (!casesToUpdate.isEmpty())
	{
		/*Update cases, partial processing*/
	    Database.saveResult[] caseResults = Database.update(casesToUpdate, false);
		
	    String subject = 'Error Updating cases with Completed_Activities__c';
	    List<String> toAddresses = new List<String>{'bspencer@appextremes.com, rdente@appextremes.com'};
	    String body = '';
	    
	    for (Database.saveResult res : caseResults)
	    {
	        if (!res.isSuccess())
	        {
	            body += 'Id = ' + res.getId() + '\n\n' + res.getErrors() + '\n\n';
	        }
	    }
	}
}
3. FPR Timestamp WFR

Criteria: ISBLANK(FPR__c) && Completed_Activities__c > 0
Evaluation: Evaluate the rule when a record is created, and any time it's edited to subsequently meet criteria
Hi there,

I have been going in circles with an issue on the Case object. The main issue is that Case fields are being updated but the values are immediately deleted.  I am new to Apex and need some help breaking this issue down because I cannot find the root cause. I'd like to simplify the solution, but I'd like to understand what is going on before I make any changes.

Here is an example of the issue:
User-added image


What is supposed to happen:
Whenever a task is completed, a trigger fires to count the number of completed tasks and add that value into the Completed Activities field. When the Completed Activities field is populated, a workflow rule fires to update the FPR field. There is another trigger that updates the Time to FPR(Business Hours) using the FPR field value. I am new to Apex and need some help breaking this issue down because I cannot find the root cause. 

Components:

1. CaseHelper Class
public static void FPRCalc(List<Case> cases, List<Case> oldList, Map<ID, sObject> newMap, Map<ID, sObject> oldMap){
		for(Case c : cases){
			if(c.FPR__c != NULL && c.BusinessHoursId !=NULL){
				if(c.Time_to_FPR_Business_Hours__c != ((Double)(BusinessHours.diff(c.BusinessHoursId, c.CreatedDate, c.FPR__c))/3600000)){
                	c.Time_to_FPR_Business_Hours__c = ((Double)(BusinessHours.diff(c.BusinessHoursId, c.CreatedDate, c.FPR__c))/3600000);
				}
            }

            if(c.Date_Time_Assigned__c != NULL && c.BusinessHoursId !=NULL){
				if(c.Time_to_Assignment_Hours__c != ((Double)(BusinessHours.diff(c.BusinessHoursId, c.CreatedDate, c.Date_Time_Assigned__c))/3600000)){
                	c.Time_to_Assignment_Hours__c = (((Double)(BusinessHours.diff(c.BusinessHoursId, c.CreatedDate, c.Date_Time_Assigned__c))/3600000));
	            }
			}
		}
	}
2. taskAndActivity Trigger
trigger taskAndActivityTrigger on Task (after insert, after update, before delete) 
{

/*	taskandactivity trigger to update Completed Activities
*/
	List<Task> taskList = new List<Task>();
	Set<Id> casesToDecrement = new Set<Id>();
	List<Id> leadIds = new List<Id>();
	Map<Id, Lead> leadIdandLead = new Map<Id, Lead>();
	Map<Id, Decimal> leadIdandCount = new Map<Id, Decimal>();
	Map<Id, Date> leadIdandMaxDate = new Map<Id, Date>();
	Map<Id, Id> leadIdandTaskId = new Map<Id, Id>();
	List<Lead> leadsToUpdate = new List<Lead>();
	Set<Id> leadsToUpdateIds = new Set<Id>();
	Map<Id, Case> caseIdandCase = new Map<Id, Case>();
	List<Id> caseIds = new List<Id>();
    Map<Id, Decimal> caseIdandCount = new Map<Id, Decimal>();
    Map<Id, Datetime> caseIdandDateTime = new Map<Id, DateTime>();
    List<Case> casesToUpdate = new List<Case>();
    Set<Id> casesToUpdateIds = new Set<Id>();

	/*Can't traverse What and Who relationships in Trigger.new, so must query for these fields. Can't use trigger.newMap in delete trigger*/
	if (!Trigger.isDelete)
	{
		taskList = [SELECT Id, Status, isClosed, What.Type, Who.Type, What.Id, Who.Id, ActivityDate, CreatedDate FROM Task Where Id IN : trigger.newMap.keyset() AND Lead_Source_Activity__c != TRUE];
		system.debug('taskList =  ' + taskList);
	}
	else
	{
		taskList = [SELECT Id, Status, isClosed, What.Type, Who.Type, What.Id, Who.Id, ActivityDate, CreatedDate FROM Task Where Id IN : trigger.oldMap.keyset() AND Lead_Source_Activity__c != TRUE];
		system.debug('taskList =  ' + taskList);
	}

	if (!taskList.isEmpty())
	{
		for (Task t : taskList)
		{

			/*Only want closed tasks/activities*/
			if (Trigger.isInsert && t.isClosed)
			{
				system.debug('Trigger.isInsert');

				if (t.What.Type == 'Case')
				{
					system.debug('What.Type = Case');

					/*Can't add to caseIdandCase map here - can't get full Case object off Task. Will query for this and add to map outside of this loop.*/ 
					caseIds.add(t.WhatId);
					system.debug('caseIds = ' + caseIds);
				}


			/*Only want to get the tasks on update that have changed Status*/
			if (Trigger.isUpdate) 
			{

				if ((Trigger.oldMap.get(t.Id).Status != Trigger.newMap.get(t.Id).Status) || (t.Status ==  'Completed' && Trigger.oldMap.get(t.Id).ActivityDate != Trigger.newMap.get(t.Id).ActivityDate))
				{
					system.debug('Trigger.isUpdate and isClosed changed');
					system.debug('new task status = ' + Trigger.newMap.get(t.Id).Status);
					system.debug('old task status = ' + Trigger.oldMap.get(t.Id).Status);

					if (t.What.Type == 'Case')
					{
						system.debug('What.Type = Case');
						/*Can't add to caseIdandCase map here - can't get full Case object off Task. Will query for this and add to map outside of this loop.*/ 
						caseIds.add(t.WhatId);
						system.debug('caseIds = ' + caseIds);
					}

					if (t.Who.Type == 'Lead')
					{
						system.debug('What.Type = Lead');
						/*Can't add to leadIdandLead map here - can't get full Lead object off Task. Will query for this and add to map outside of this loop.*/  
						leadIds.add(t.WhoId);
						system.debug('leadIds = ' + leadIds);
					}
				}
			}

			if (Trigger.isDelete)
			{
				if (t.What.Type == 'Case' && t.isClosed == TRUE)
				{
					leadIds.add(t.WhoId);
					system.debug('leadIds = ' + leadIds);
					system.debug('What.Type = Case');
					casesToDecrement.add(t.WhatId);
					system.debug('casesToDecrement = ' + casesToDecrement);
				}
				if (t.Who.Type == 'Lead' && t.isClosed == TRUE)
				{
					leadIds.add(t.WhoId);
					system.debug('leadIds = ' + leadIds);
					system.debug('Who.Type = Lead');
					leadIdandTaskId.put(t.Id, t.WhoId);
					system.debug('leadIdandTaskId = ' + leadIdandTaskId);
				}
			}
		}
	}

	/*Build key-value pairs from list of ids obtained in trigger context, since task --> case relationship can't be traversed*/ 
	if (!caseIds.isEmpty())
	{
		for (Case c : [SELECT Id, Last_Activity__c, Completed_Activities__c FROM Case WHERE Id IN : caseIds])
		{
			caseIdandCase.put(c.Id, c);
		}
	}

    if (!caseIdandCase.isEmpty())
    {
	    AggregateResult[] caseAggregate = [SELECT WhatId, Count(Id)closedTaskCount FROM Task WHERE isCLosed = TRUE AND What.Type = 'Case' AND WhatId IN : caseIdandCase.keyset() GROUP BY WhatId];
	    system.debug('caseAggregate =  ' + caseAggregate);

	    if (!caseAggregate.isEmpty())
	    {
	    	for (AggregateResult ca : caseAggregate)
	    	{
	    		caseIdandCount.put((ID)ca.get('WhatId'), (Decimal)ca.get('closedTaskCount'));
	    		system.debug('caseIdandCount = ' + caseIdandCount);
	    	}

	    	if (!caseIdandCount.isEmpty())
	    	{
	    		for (Case c : [SELECT Id, Completed_Activities__c FROM CAse WHERE Id IN : caseIdandCount.keyset()])
	    		{
	    			caseIdandCase.get(c.Id).Completed_Activities__c = caseIdandCount.get(c.Id);
	    		}
	    	}
	    }

		/*If task is updated from isClosed to !isClosed, and it is the only task related to that case, we need to decrement it. This list also contains deleted deleted case tasks.*/
		else if (caseAggregate.isEmpty())
		{
			for (Id caseId : caseIdandCase.keySet())
			{
				casesToDecrement.add(caseId);
				system.debug('casesToDecrement = ' + casesToDecrement);
			}
		}

		if (!caseIdandDateTime.isEmpty())
		{
			for (Case c : [SELECT Id, Last_Activity__c, Next_Business_Day_After_Last_Activity__c, BusinessHoursId FROM Case Where Id IN : caseIdandDateTime.keySet()])
			{
				caseIdandCase.get(c.Id).Last_Activity__c = caseIdandDateTime.get(c.Id);
				system.debug('caseIdandCase.get(c.Id).Last_Activity__c ' + caseIdandCase.get(c.Id).Last_Activity__c);
				dateTime tempDateTime = caseIdandDateTime.get(c.Id);
				system.debug('tempDateTime = ' + tempDateTime);

				if (c.BusinessHoursId != null && tempDateTime != null)
				{
					BusinessDays bizDays = new BusinessDays(c.BusinessHoursId);
					system.debug('bizDays = ' + bizDays);
					caseIdandCase.get(c.Id).Next_Business_Day_After_Last_Activity__c = BizDays.nextBusinessDay(date.newInstance(tempDateTime.year(), tempDateTime.month(), tempDateTime.day()));
					system.debug('caseIdandCase.get(c.Id).Next_Business_Day_After_Last_Activity__c = ' + caseIdandCase.get(c.Id).Next_Business_Day_After_Last_Activity__c);
				}
			}
		}

		if (!casesToDecrement.isEmpty())
		{
			for (Case c : [SELECT Id, Completed_Activities__c FROM Case WHERE Id IN : casesToDecrement])
			{
				caseIdandCase.get(c.Id).Completed_Activities__c = caseIdandCase.get(c.Id).Completed_Activities__c - 1;
			}
		}
	}

	/*Loop through the key-value pairs and add them to the list of Cases to pass into the saveResult - only List of sObjects accepted, and don't want dups, hence the set.*/
    for (Id caseId : caseIdAndCase.keySet())
    {
        if (!casesToUpdateIds.contains(caseId))
        {
            casesToUpdate.add(caseIdAndCase.get(caseId));
            system.debug('casesToUpdate = ' + casesToUpdate);
        }
        else
        {
            casesToUpdateIds.add(caseId);
            system.debug('casesToUpdateIds = ' + casesToUpdateIds);
        }
    }

	if (!casesToUpdate.isEmpty())
	{
		/*Update cases, partial processing*/
	    Database.saveResult[] caseResults = Database.update(casesToUpdate, false);
		
	    String subject = 'Error Updating cases with Completed_Activities__c';
	    List<String> toAddresses = new List<String>{'bspencer@appextremes.com, rdente@appextremes.com'};
	    String body = '';
	    
	    for (Database.saveResult res : caseResults)
	    {
	        if (!res.isSuccess())
	        {
	            body += 'Id = ' + res.getId() + '\n\n' + res.getErrors() + '\n\n';
	        }
	    }
	}
}
3. FPR Timestamp WFR

Criteria: ISBLANK(FPR__c) && Completed_Activities__c > 0
Evaluation: Evaluate the rule when a record is created, and any time it's edited to subsequently meet criteria