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
Kenji775Kenji775 

Not sure how to logically approach this. Not a syntax issue, but a logic issue

Hey all,

 

So I have come to a bit of an impass in a trigger I am writting. It may be because it's early and I can't think yet, but I am just not quite sure how to logically solve this problem.

 

Here is the deal, this trigger is responsible for updating a contact associated with a task. When the task is saved, the status field is checked. If the task is completed, an associated date field on the related contact is set to the completion date on the task (if the date to be used for the update is later than the one currently existing). This is to keep track of things such as 'Last stay in touch call', 'Last meeting', etc. The tasks represent interactions with a contact, and when the task is completed they want to mark the contact so they know the last time that kind of action was performed. I have this working perfectly currently.

 

Where it gets complicated is if a task is deleted. They have decided that they want the information to 'roll back' if a task is deleted. They want the field on the contact to be 'recalculated' to find the task that is now the newest that applies to the field that just got deleted. So if the most recent task that set the 'last stay in touch call' gets deleted, they want the system to find the next most recent and use that. This is where I am a bit stuck. The only approach I can think of (while keeping it bulk friendly) is something like:

 

1) Query for all tasks associated with any contact that appeared in this list of newly deleted tasks.
 

2) Iterate over every task. Look to see if the 'Activity_Type__c' matches that of the task that just got deleted for this contact.
 

3) If it is a match on the field, check to see if it most recent than any other entry (could probably eliminate this by sorting the query by the date, and skipping duplicates in my loop). 

 

4) Using the list of tasks, continue with the trigger logic as normal.

 

The issue I have here is, what if one contact is getting multiple tasks deleted at one time? Because when I am iterating through the list of tasks for every contact, I'd have to iterate over every task for them in the trigger context, then find a matching task in the query I just ran, and... argh it gets so complicated and cumbersom. Also, this approach seems excruciatingly inefficient. Does anyone have any better ideas? Below is my code thus far so you can see where I am at. Thanks so much!

 

/**********************************************
Name: ContactActivityRecordTrigger
Author: Daniel Llewellyn
Date 3/14/2012
Description: Will update a contact related to a task when a task is completed. The contact has various date fields that may
                          be populated based on the type of task. Updates performed by the elquoa marketing tool or anyone from 
                          marketing do not fire these updates
***********************************************/                          

trigger ContactActivityRecordTrigger on Task(after insert, after undelete, after update) 
{
    
    try
    {    
        list<Task> tasks;
        if(Trigger.Isundelete)
        {
            tasks = trigger.old;
        }
        else
        {
            tasks = trigger.new;
        }
        //create map of contacts that will contain contacts to update
        map<Id,Contact> contactMap = new map<id,Contact>();
        
        //create a map of users that contain the names of the users related to the tasks.
        map<Id,User> userMap = new map<id,User>();
        
        //we will need to find the DS_Role__c field for all the contacts. So create a map of the contact id, to the contact record
        //so we can run one query, and populate them map, then get the contact details when we need them later without needing
        //to have a query in our loop.
        for (Task thisTask: tasks) 
        {
            //we are only interested in this task if it has a contact, so no contact, just skip adding an entry for this task.
            if(thisTask.WhoId != null)
            {
                contactMap.put(thisTask.WhoId,null);
            }
            if(thisTask.OwnerId != null)
            {
                 userMap.put(thisTask.OwnerId,null);
            }
        }
        
        //populate the map with the id of the contact, then the details about that contact.
        for(Contact con : [select id, DS_Role__c,Last_Meeting_Sales_Call__c,Last_Call__c,Last_Email__c,Last_Demo__c,Last_Mass_Email__c,Last_Sent_Info__c,Last_Marketing_Activity__c from contact where id in :contactMap.keySet()])
        {
            contactMap.put(con.id,con);
        }
    
        //populate the map with the id of the contact, then the details about that contact.
        for(User usr : [select id, Name from User where id in :userMap.keySet()])
        {
            userMap.put(usr.id,usr);
        }
       
       //if this is a delete trigger, the current list of tasks has actually been deleted, so we need to find
       //the task that is now the most recent for each user of the same type that just got deleted
       if(trigger.isDelete)
       {
           //find all the tasks for all the users
           list<task> allTasks = [select id, WhoId, OwnerId,Status,Activity_Type__c, ActivityDate from task where whoId in :contactMap.keySet() order by ActivityDate desc ];
           
           //so now I have to loop over all the tasks I just fetched, and then find all the tasks for the associated contact and see if there is a match and.... arg I have no idea.
           for(Task task : allTasks)
           {
           
           }
       }
       
        //iterate over every task passed in
        for (Task thisTask: tasks)     
        {
            //if this task does not have a contact related to it, then just skip this task and continue.
            if(thisTask.WhoId == null)
            {
                continue;
            }    
            
             //create a reference to the contact associated with this task that we will update
            Contact thisContact =contactMap.get(thisTask.WhoId);
    
            //create a reference to the owner associate with this task
            User thisUser = userMap.get(thisTask.OwnerId);
           
            date activityDate;
            if( thisTask.ActivityDate != null)
            {            
                activityDate = thisTask.ActivityDate;
            }
            else
            {
                activityDate = Date.newInstance(thisTask.LastModifiedDate.year(),thisTask.LastModifiedDate.month(),thisTask.LastModifiedDate.day()); 
            }
            //check if the task status is completed
            if (thisTask.Status.toLowerCase() == 'completed') 
            {                
                //make sure the owner of the task is not eloqua marketing, and make sure the contact's role is not marketing/communications
                if (thisUser.Name.toLowerCase() != 'eloqua marketing' && thisContact.DS_Role__c != 'marketing/communications') 
                {
                    if (thisTask.Activity_Type__c == 'Meeting/Sales Call' && (activityDate > thisContact.Last_Meeting_Sales_Call__c || thisContact.Last_Meeting_Sales_Call__c == null) ) 
                    {
                        thisContact.Last_Meeting_Sales_Call__c = activityDate;
                    }
                    else if (thisTask.Activity_Type__c == 'Call' && (activityDate > thisContact.Last_Call__c ||  thisContact.Last_Call__c == null))
                    {
                        thisContact.Last_Call__c = activityDate;
                    }
                    else if (thisTask.Activity_Type__c == 'Email' && (activityDate > thisContact.Last_Email__c || thisContact.Last_Email__c == null))
                    {
                        thisContact.Last_Email__c = activityDate;
                    }
                    else if (thisTask.Activity_Type__c == 'Demo' && (activityDate > thisContact.Last_Demo__c || thisContact.Last_Demo__c == null)) 
                    {
                        thisContact.Last_Demo__c = activityDate;
                    }
                    else if (thisTask.Activity_Type__c == 'Mass Email' && ( activityDate > thisContact.Last_Mass_Email__c || thisContact.Last_Mass_Email__c == null)) 
                    {
                        thisContact.Last_Mass_Email__c = activityDate;
                    }
                    else if (thisTask.Activity_Type__c == 'Sent Info' && ( activityDate > thisContact.Last_Sent_Info__c || thisContact.Last_Sent_Info__c == null ))
                    {
                        thisContact.Last_Sent_Info__c = activityDate;
                    }
                    else 
                    {
                        if (thisTask.ActivityDate > thisContact.Last_Marketing_Activity__c || thisContact.Last_Marketing_Activity__c == null) 
                        {
                            thisContact.Last_Marketing_Activity__c = activityDate;
                        }          
                    }
                }
            }
            contactMap.put(thisContact.id,thisContact);
        }
   
        //we don't need an all or nothing update, so use database.update with the all or nothing flag set to false
        if(!contactMap.values().isEmpty())
        {
            database.update(contactMap.values(),false);
        }
    }    
    catch(exception e)
    {
        system.debug('Error occured updating related contact' + e);
        
        //Send an email to the admin after an error occures
        Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage();
        String[] toAddresses = new String[] {'Kenji776@gmail.com'};
        mail.setToAddresses(toAddresses);
        mail.setSubject('Error in ContactActivityRecordTrigger');
        mail.setPlainTextBody('An error occured while running the trigger.' + e.getMessage() + ' On Line Number ' + e.getLineNumber());
        Messaging.sendEmail(new Messaging.SingleEmailMessage[] { mail });        
    }    
    
}

 

BrianWKBrianWK

I like it! 

Fun logic issue.

 

Okay so I'm going to re-rattle off your description to make sure I understood your issue.

 

Concern: User deletes a completed task. The "last stay in touch call" date of related contact then must be updated with the most recent completed task's date. Ultimate goal: The associated dates on the Contact reflect the most-recent date from a completed task for specific activity types.

 

Your list is correct, but I think there might be a different method that might be easier. Effectively - you don't care what the deleted task is as long as its related contact has the correct dates.

 

So instead of trying to update for a single activity type - how about creating a class that identifies the "Max Date" from all the tasks for each activity date you're stamping on the Contact? Then you could either make it Asynchronous or flag the contact for review and schedule a Batch Apex (assuming you're okay with a "nightly" update)

 

So then your logic becomes:

 

1. Task is deleted that meets criteria

2. Contact for task is past to your "Max Date" class

3. Max Date class reviews and updates all date fields

 

The benefit of this is two-fold. One you don't have to worry about people mass-deleting tasks. Two - you can set it up as a sanity/validity check by having your contacts get reviewed on a scheduled basis.

 

 

 

Kenji775Kenji775

Brian,

 

Yes you have correctly understood the issue. I do like your solution of just saying, screw it, lets update all the fields at once. The only issue is that I am not sure my client in this case would want to go with a nightly batch update. I could just build the entire update into the trigger, or maybe use an async method. It seems unlikely they would ever mass delete tasks, I think it's all just done through button click in the UI so I'm not terribly worried about it, but I still want to account for it, ya know?

 

I can certainly present this solution to my client and see what they think. Thanks a bunch!

 

BrianWKBrianWK

You're welcome. I love these puzzles. I often bang my head against the table trying to think of the best way to do this.

 

Keep in mind you don't have to restrict yourself to just trigger or aynch.... You could set this up so if the incoming items are within governor limits to do it Synchronously, Asych and/or batch it.

 

There was a really good session at Dreamforce 2012 that had a great example of code using this method. I beleive it was Patterns for Developers - something similar in title.

 

So in you scenario if you only get 1 task you can update the Contact right away. If you get more than x contact you update Asynchronously... and then you have a scheduled batch as a safety net.

sfcksfck

Here's an idea - 

 

- Use an After Delete trigger 

- Retrieve a list of all affected contacts, with tasks attached using the child relationship, ordered by activity type and date (because you are doing this After delete, records that have been deleted will not come up so you needn't worry about that)

- Loop through the list of contacts, and for each one do an inner loop through the related tasks, setting the fields ...

 

Just a sketch - if think it might work I am happy to help you out with making it more specific

 

Starz26Starz26

alternatly if you need to do it in a before delete for some reason,

 

in your trigger, search for all tasks that are related to the contact but NOT in the id trigger keyset...

dmchengdmcheng

You could also call a method that runs SOQL aggregate queries on the task types with MAX() on the date field and grouped by Contact, then change all the date fields in the contact record and update.

 

It's brute force and will probably break if there are large quantities of contact and task records,  but it's simple to code.