1

I am having issues getting sucess with my account trigger. Would really appreciate someone taking a look.

I can't even get a compile :(

The requirement I am trying to code for is this.

If I have contacts owned by a difference user that the Account Owner, if Account Owner is changed, do not update contact owners to the new Account owner. In other words, retains contact owners on change of account owner.

TRIGGER

    trigger sbox_Account on Account (
    before insert, after insert, 
    before update, after update, 
    before delete, after delete) {   

    Map<Id,String> contactsPendingOwnershipChange = new Map<Id,String>();

    if (trigger.isBefore) {     
        if (trigger.isInsert) {}
        if (trigger.isUpdate) {
            contactsPendingOwnershipChange = new sbox_ContactUtil.getContactsPendingOwnershipChange(Trigger.new, Trigger.oldMap);
        }
        if (trigger.isDelete) {}
    }

    if (trigger.IsAfter) {
        if (trigger.isInsert) {} 
        if (trigger.isUpdate) { 
           sbox_ContactUtil.restoreContactOwnership(contactsPendingOwnershipChange);
        }
        if (trigger.isDelete) {}
    }                     
}

CLASS

public with sharing class sbox_ContactUtil {
    /** This class is a helper class for the Account trigger. 
    There is a default feature in SFDC that triggers when an Account's Owner is changed 
    (not via the API or Workflow automation) which cascades down ownership from the new 
    Account owner, to certain related object records. One such record are the related Contacts.
    This would be fine, expect it also skips re-firing process builder, and flows
    (https://help.salesforce.com/articleView?id=000323827&language=en_US&type=1&mode=1). 
    Therefore the only solution was to create a trigger to reverse these changes.
    **/

    public static Map<Id,String> getContactsPendingOwnershipChange(List<Account> newRecords, Map<Id, Account> oldMap)
    {
        Map<Id,String> contactsPendingOwnershipChange = new Map<Id,String>();
        for (Account a : newRecords)
        {
            if (a.OwnerId != oldMap.get(a.Id).OwnerId)
            {
                //This Account has had an ownership change, so all Contacts related to this Account 
                //Will automatically have their owner changed to the new owner.
                for (Account acc : [SELECT Id, OwnerId, (SELECT Id, OwnerId FROM Contacts) FROM Account WHERE Id = :a.Id])
                { 
                    for (Contact c : acc.Contacts)
                    {
                        if (c.OwnerId != acc.OwnerId) 
                        {
                            //These Contacts are not owned by the old owner, therefore we must preserve their ownership.
                            contactsPendingOwnershipChange.put(c.Id, c.OwnerId);
                        }
                    }
                }
            }
        }
        return contactsPendingOwnershipChange;
    }

    public static void restoreContactOwnership(Map<Id,String> contactsPendingOwnershipChange)
    {
        //Create and array to store the Contact records that will need to be restored.
        Contact[] contactsToRestoreOwnership = new Contact[0];

        if (!contactsPendingOwnershipChange.isEmpty())
        {
            for (Id key : contactsPendingOwnershipChange.keyset())
            { 
                Contact restoreContact = new Contact(OwnerId = contactsPendingOwnershipChange.get(key), Id = key);
                contactsToRestoreOwnership.add(restoreContact);
            } 
            update contactsToRestoreOwnership;
        }
    }

}

TEST CLASS

@isTest
public class sbox_test_Account {
    @isTest static void testAccountTrigger()
    /**
    Create 3 Users
    Create Account and Contact
    Update Contact Owner to User 3
    Update Owner of Account to User 2
    Validate that Contact Over is restored to User 3
    **/
    {
        Integer numUsers = 3;
        Integer numAccts = 1;
        Integer numContactsPerAcct = 3;

        List<User> testUsers = new List<User>();
        List<Contact> testContacts = new List<Contact>();
        String newUserRoleId = 'Sales Representative';
        Profile p = [SELECT Id FROM Profile WHERE Name='Custom - Sales User'];

        UserRole r = new UserRole(DeveloperName = 'MyCustomRole', Name = 'My Role');
        insert r;

        for(Integer i=0;i<numUsers;i++) {
            User u = new User();
            u.LastName = 'User '+i;
            u.Alias = 'U'+i;
            u.CommunityNickname = 'U'+i;
            u.Email = 'user '+i+'@test.com';
            u.Username = 'username'+i;
            u.UserRoleId = r.Id;
            u.ProfileId = p.Id;
            insert u;
            testUsers.add(u);
            System.debug('created test user: '+i);
        }

            Account a = new Account();
            a.RecordTypeId = Schema.SObjectType.Account.getRecordTypeInfosByName().get('Prospect').getRecordTypeId();
            a.Name = 'Test Account';
            a.BillingStreet = 'Level 99, 999 ThisRd';
            a.BillingCity = 'City';
            a.BillingState = 'Victoria';
            a.BillingPostalCode = '3000';
            a.BillingCountry = 'Australia';
            a.Phone = '+61 99999999';
            a.Website = 'website.com.au';
            a.Type = 'Prospect';
            a.OwnerId = testUsers[0].Id; //Assigned Accounts to Owner 0
            insert a;
            System.debug('created test account');

        for (Integer k=0;k<numContactsPerAcct;k++) {
            Contact c = new Contact();
            c.FirstName = 'Test '+k;
            c.LastName  = 'Test '+k;
            c.AccountId = a.id;
            c.OwnerId = testUsers[0].Id;    //Assigned all Contacts to Owner 0  
            insert c;
            testContacts.add(c);
            System.debug('created contact: '+k);    
        }

        //Now update the a contact to be owned by another User      
        testContacts[2].OwnerId = testUsers[2].id;
        update testContacts[2];
        System.debug('Updated contact owner for test Cotnact 2');

        //Now update the owner of the Account
        a.OwnerId = testUsers[1].id;
        update testUsers[1];
        System.debug('Updated Account owner');

        //Validate that Contact Over is restored to User 3
        System.assertEquals(testContacts[0].OwnerId, testUsers[1].Id);
        System.assertEquals(testContacts[1].OwnerId, testUsers[1].Id);
        System.assertEquals(testContacts[2].OwnerId, testUsers[2].Id);
    }
}

Errors:

  • Invalid type: sbox_ContactUtil.getContactsPendingOwnershipChange (11:46)

UPDATE

I am having issues getting success with my account trigger.

It's worth pointing out that I have a background in programming, but I am not a full time dev. This is basically my first attempt at Apex and triggers.

Would really appreciate someone taking a look.

At first I could not get it to compile, but now it seems more likely a logic issue. I've been using the debugger in VSCode, but I have a suspicion.

The documentation around this automated ownership cascading, indicates that this does not apply if the Account owner is updated by API, or dataloader. Is it possible that the test execution falls into this, thus it's not actually triggering the cascading ownership, like it would if a user modified the Account owner via the UI?

Any way, here is where I am at now, after making some of the improvements offered by David.

The requirement I am trying to code for is this.

If I have contacts owned by a difference user that the Account Owner, if Account Owner is changed, do not update contact owners to the new Account owner. In other words, retains contact owners on change of account owner.

TRIGGER

trigger sbox_Account on Account (before update, after update) {   

    static Map<Id,String> contactsPendingOwnershipChangeResults = new Map<Id,String>();

    if (trigger.isBefore) {     
        if (trigger.isUpdate) {
           contactsPendingOwnershipChangeResults = sbox_ContactUtil.getContactsPendingOwnershipChange(Trigger.new, Trigger.oldMap);
        }
    }

    if (trigger.IsAfter) {
        if (trigger.isUpdate) { 
           sbox_ContactUtil.restoreContactOwnership(contactsPendingOwnershipChangeResults);
        }
    }                     
}   

CLASS

public with sharing class sbox_ContactUtil {
    /* This class is a helper class for the Account trigger. 
    There is a default feature in SFDC that triggers when an Account's Owner is changed 
    (not via the API or Workflow automation) which cascades down ownership from the new 
    Account owner, to certain related object records. One such record are the related Contacts.
    This would be fine, expect it also skips re-firing process builder, and flows
    (https://help.salesforce.com/articleView?id=000323827&language=en_US&type=1&mode=1). 
    Therefore the only solution was to create a trigger to reverse these changes.
    */

    public static Map<Id,String> getContactsPendingOwnershipChange(Account[] newAccounts, Map<Id, Account> oldMap)
    {
        //Create a map of Contact Id's and the Contact's Old Owner, to return from this method. 
        Map<Id,String> contactsPendingOwnershipChange = new Map<Id,String>();

        //Iterate of the List of Accounts from trigger.new
        for (Account a : newAccounts)
        {
            //Check if the Account owner has changed.
            if (a.OwnerId != oldMap.get(a.Id).OwnerId)
            {
                /*This Account has had an ownership change, so all Contacts related to this Account 
                will automatically have their owner changed to the new owner. So get all of the Contacts,
                then iterate through and if their old owner is not the same and the Account's new owner, 
                add them to our Map of contacts that we will need to restore. Note, I assume that the Contact 
                owner has not yet been changed by the default ownership change functionality, given this 
                method was called before update. */
                for (Account acc : [SELECT Id, OwnerId, (SELECT Id, OwnerId FROM Contacts) FROM Account WHERE Id = :a.Id])
                { 
                    for (Contact c : acc.Contacts)
                    {
                        if (c.OwnerId != acc.OwnerId) 
                        {
                            //These Contacts are not owned by the old owner, therefore we must preserve their ownership.
                            contactsPendingOwnershipChange.put(c.Id, c.OwnerId);
                        }
                    }
                }
            }
        }
        return contactsPendingOwnershipChange;
    }

    public static void restoreContactOwnership(Map<Id,String> contactsPendingOwnershipChange)
    {
        //This method is called after update.
        //Create and array to store the Contact records that will need to be restored.
        Contact[] contactsToRestoreOwnership = new Contact[0];

        for (Id key : contactsPendingOwnershipChange.keyset())
        { 
            Contact restoreContact = new Contact(OwnerId = contactsPendingOwnershipChange.get(key), Id = key);
            contactsToRestoreOwnership.add(restoreContact);
        } 
        try {
            update contactsToRestoreOwnership;
        } catch (DmlException e) {
            System.debug('An unexpected error has occurred: ' + e.getMessage());
        }
    }
}

TEST CLASS

@isTest
public class sbox_test_Account {
    @isTest static void testAccountTrigger()
    /**
    Create 3 Users
    Create Account and 3x Contacts
    Update Contact Owner to User 3
    Update Owner of Account to User 2
    Validate that Contact Over is restored to User 3
    **/
    {
        Integer numUsers = 3;
        Integer numAccts = 1;
        Integer numContactsPerAcct = 3;

        List<User> testUsers = new List<User>();
        List<Contact> testContacts = new List<Contact>();

        String newUserRoleId = 'Sales Representative';
        Profile pf = [SELECT Id FROM Profile WHERE Name='System Administrator'];

        UserRole ur = new UserRole(DeveloperName = 'MyTestRole', Name = 'MyTestRole');
        insert ur;

        for(Integer i=0;i<numUsers;i++) {
            User usr = sbox_TestUtil.createTestUser(ur.Id, pf.Id, 'Test FirstName'+i, 'Test LastName'+i);
            insert usr;
            testUsers.add(usr);
            System.debug('created test user: '+i+'Id = '+testUsers.get(i).Id);
        }

        System.runAs(testUsers.get(0)){

            Account a = new Account();
            a.RecordTypeId = Schema.SObjectType.Account.getRecordTypeInfosByName().get('Prospect').getRecordTypeId();
            a.Name = 'Smops Test';
            a.BillingStreet = 'Level 99, 999 Test Rd';
            a.BillingCity = 'Test City';
            a.BillingState = 'Victoria';
            a.BillingPostalCode = '3146';
            a.BillingCountry = 'Australia';
            a.Phone = '+61 3 9882 6909';
            a.Website = 'website.com.au';
            a.Type = 'Prospect';
            a.OwnerId = testUsers.get(0).Id; //Assigned Accounts to Owner 0
            insert a;
            System.debug('created test account. OwnerId= '+a.OwnerId);

            for (Integer k=0;k<numContactsPerAcct;k++) {
                Contact c = new Contact();
                c.FirstName = 'Test'+k;
                c.LastName  = 'Test'+k;
                c.AccountId = a.id;
                c.OwnerId = testUsers.get(0).Id;    //Assigned all Contacts to Owner 0  
                insert c;
                testContacts.add(c);
                System.debug('created contact: '+k+'. OwnerId = '+c.OwnerId);   
            }

            //Now update the a contact to be owned by another User      
            testContacts[2].OwnerId = testUsers.get(2).id;
            update testContacts.get(2);
            System.debug('Updated contact owner for test Contact 2. New OwnerId ='+testContacts.get(2).OwnerId);

            //Now update the owner of the Account
            a.OwnerId = testUsers.get(1).id;
            update a;
            System.debug('Updated Account OwnerId ='+a.OwnerId);

            for(Integer m=0;m<testContacts.size();m++)
            {
              System.debug('Related Contact '+m+' OwnerId = '+testContacts.get(m).OwnerId);
            }  
        }    

        Contact c0 = [SELECT Id, OwnerId FROM Contact WHERE Id = :testContacts.get(0).Id];
        Contact c1 = [SELECT Id, OwnerId FROM Contact WHERE Id = :testContacts.get(1).Id];
        Contact c2 = [SELECT Id, OwnerId FROM Contact WHERE Id = :testContacts.get(2).Id];

        //This first test just ensures that the automatic cascade of ownership from Account updated Contact 0 to the new Account Owner.
        System.assert(c0.OwnerId == testUsers.get(1).Id, 'Error: Contact 0 Owner has been modfied by the system on Account Owner change to User 1');

        //This also simply tests that the automatic cascade of ownership from Account updated Contact 0 to the new Account Owner.
        System.assert(c1.OwnerId == testUsers.get(1).Id, 'Contact 1 Owner has been modfied by the system on Account Owner change to User 1');

        //This last test checks to verify that we restored the Contact owner to it's prior owner.
        System.assert(c2.OwnerId == testUsers.get(2).Id, 'Contact 2 Owner has NOT been modfied by the system on Account Owner change and remains owned by User 2');

    }
}

TEST UTIL CLASS

@isTest
public class sbox_TestUtil {

    public static User createTestUser(Id roleId, Id profID, String fName, String lName)
    {
        String orgId = UserInfo.getOrganizationId();
        String dateString = String.valueof(Datetime.now()).replace(' ','').replace(':','').replace('-','');

        Integer randomInt = Integer.valueOf(math.rint(math.random()*1000000));
        String uniqueName = orgId + dateString + randomInt;
        User tuser = new User(  firstname = fName,
                                lastName = lName,
                                email = uniqueName + '@test' + orgId + '.org',
                                Username = uniqueName + '@test' + orgId + '.org',
                                EmailEncodingKey = 'ISO-8859-1',
                                Alias = uniqueName.substring(18, 23),
                                TimeZoneSidKey = 'America/Los_Angeles',
                                LocaleSidKey = 'en_US',
                                LanguageLocaleKey = 'en_US',
                                ProfileId = profId,
                                UserRoleId = roleId);
        return tuser;
    }

}

Errors:

  • This compiles, however the tests are failing. I believe this may be due to my logic, even if the syntax is now throwing an error.
    • My debug logs report that after all of the execution of the test, that the Contact Owner for all three contacts are still Owned by the user[0], so this makes me think that maybe the system automation to update their owners on the Account owner change is not happening in this environment, vs if a user changed the owner in the UI???
    • I understand bulification, but I'm unsure how to approach the account bulkification you mentioned. Any guidance would be appreciated.

1 Answer 1

3

There's quite a few issues to look at here.

Syntax Error

The syntax error is because you're missing a () to create an instance of sbox_ContactUtil with new:

contactsPendingOwnershipChange = new sbox_ContactUtil.getContactsPendingOwnershipChange(Trigger.new, Trigger.oldMap);

However, that would be invalid for a different reason: that method is static, so it is called on the class, not an instance:

contactsPendingOwnershipChange = sbox_ContactUtil.getContactsPendingOwnershipChange(Trigger.new, Trigger.oldMap);

Bulkification

You're running one SOQL query per Account impacted. This logic needs to be bulkified to run a single query for all impacted Accounts, then then postprocess the data into your storage structure.

Data Persistence

This variable's lifespan is a single trigger invocation. It does not persist across events:

trigger sbox_Account on Account (
before insert, after insert, 
before update, after update, 
before delete, after delete) {   

Map<Id,String> contactsPendingOwnershipChange = new Map<Id,String>();

If you want to store data across trigger invocations/events, you must persist it as a static variable in a handler class.

Irrelevant Code

trigger sbox_Account on Account (
before insert, after insert, 
before update, after update, 
before delete, after delete) {   

There's no reason to run this trigger on events other than update.

    if (trigger.isInsert) {}
    if (trigger.isDelete) {}

These branches do nothing. There's no reason to have them.

    if (!contactsPendingOwnershipChange.isEmpty())

Iterating over an empty set is a no-op. You don't need to check first.

Test Issues

        u.Username = 'username'+i;

Usernames should be in the form of an email address and must incorporate random elements because they are required to be globally unique across Salesforce. Add a random number and the current date-time in seconds, plus a project-specific token, to ensure you don't get collisions.

You are going to get a Mixed DML error because you're combining setup and non-setup object DML in the same transaction context.

For example, you can’t update an account and a user role in a single transaction.

You'll need to use a System.runAs() block to get around this limitation.

    //Validate that Contact Over is restored to User 3
    System.assertEquals(testContacts[0].OwnerId, testUsers[1].Id);
    System.assertEquals(testContacts[1].OwnerId, testUsers[1].Id);
    System.assertEquals(testContacts[2].OwnerId, testUsers[2].Id);

You have to re-query these records to get changes from the database so that you can validate them.

Kudos on writing what looks like a well-structured unit test, however!

4
  • Thanks David, I've made some modifications and will update my opening post with the changes. If you could take another look I still need a little more help, but I think I'm really close. Commented Jan 10, 2020 at 5:33
  • Also, when you said "If you want to store data across trigger invocations/events, you must persist it as a static variable in a handler class", do you mean that the return value from that function needs to be static, or the map I declare at the top of the trigger? Thanks again so much Commented Jan 10, 2020 at 6:59
  • @SeanRichards The data has to be stored in a variable that is declared static in the body of a class. It cannot be declared in the trigger body.
    – David Reed
    Commented Jan 10, 2020 at 14:39
  • You're most of the way there on bulkification. You just need to collect Account Ids and run a single Account query, not doing it inside a for loop.
    – David Reed
    Commented Jan 11, 2020 at 0:18

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .