+ Start a Discussion
Forza di SognoForza di Sogno 

Duplicates from Web2lead

I have a PHP page that sends the Web2Lead information to Salesforce (the data goes to contacts, not leads).  In Salesforce I have a trigger that prevents duplicates from being inserted, based on the key of email address, lead source, and creation date.  In the sandbox and in Production this works fine and when I go to the landing page and try to create a duplicate, it won't let me (this is good).  However, every now and then duplicate contacts are still being created (at the exact same time).

 

Below is the trigger I'm using.  What can I do to effectively prevent duplicates from being inserted?

trigger AvoidWebleadDuplicatesTrigger on Contact (before insert)
{
    set <string> setEmail = new set<string>();
    set <string> setLeadSource = new set<string>();

    list <contact> currentcontacts = new list<contact>();

    for(contact acc:trigger.new)
    {
        setEmail.add(acc.Email);
        setLeadSource.add(acc.LeadSource);
    }   

    currentcontacts =   [select Email,LeadSource,CreatedDate,id
                        from contact
                        where Email in:setEmail and LeadSource in:setLeadSource and CreatedDate = TODAY];

    for(contact acc:trigger.new)  
    {
        if( currentcontacts.size() > 0 )
            acc.adderror('This contact already exists: ' + acc.Email + ' - ' + acc.LeadSource );
    }

}

 

Best Answer chosen by Forza di Sogno
sfdcfoxsfdcfox

If you're using the standard web to lead form, and the user submits more than one record at a time (e.g. double-clicking the submit button), those items may be queued together into the same trigger invocation. This means that a proper trigger should also check incoming records against each other as well to verify that there aren't in-flight dupes. You can do this by adding the values to a set. Here's one possible version:

 

trigger X on Contact (before insert) {
	Map<String, Map<String, Contact>> contacts = new Map<String, Map<String, Contact>>();
	// Step 1: Map in-flight contacts, prevents in-flight duplicates
	for(Contact record: Trigger.new) {
		String escapedSource = String.escapeSingleQuotes(record.leadSource),
			   escapedEmail = String.escapeSingleQuotes(record.email);
		if(!contacts.containsKey(escapedSource)) {
			contacts.put(escapedSource, new Map<String, Contact>());
		}
		if(contacts.get(escapedSource).containsKey(escapedEmail)) {
			record.addError('Duplicate Lead detected.');
		} else {
			contacts.get(escapedSource).put(escapedEmail, record);
		}
	}
	// Step 2: Prepare query to check for duplicates
	String[] queryFilters = new String[0];
	for(String source: contacts.keySet()) {
		queryFilters.add(
			String.format('(LeadSource = '{0}' AND Email IN (\'\'{1}\'\')',
				new String[] {
					source,
					String.join(new List<String>(contacts.get(source).keySet()),'\',\'')
				}
			)
		);
	}
	// Query should look like:
	// SELECT Id, Email, LeadSource FROM Contact WHERE CreatedDate = TODAY AND
	//	(LeadSource = 'Source1' AND Email IN ('email1','email2') OR (LeadSource = 'Source2' AND Email IN ('email3','email4')))
	
	// Step 3: Query existing records, mark in-flight records as duplicates.
	for(Contact record:Database.query('SELECT Id, Email, LeadSource FROM Contact WHERE CreatedDate = TODAY AND ('+String.join(queryFilters,' OR ')+')')) {
		contacts.get(String.escapeSingleQuotes(record.LeadSource)).get(String.escapeSingleQuotes(record.Email)).addError('Duplicate Lead detected');
	}
}

 

All Answers

Sonam_SFDCSonam_SFDC

Hi Forza,

 

When you same it is creating the duplicate contact at the same time - I am sensing that the form is being submitted twice and this might be an issue at the form end..

Did you try to find if there might be an issue there?

 

 

Forza di SognoForza di Sogno

Hi Sonam,

 

I agree that this is probably an issue on the website's end, but I'm trying to deal with the issue from the Salesforce end, which is why I wrote the trigger to prevent duplicates from being inserted.  I'm stumped - the trigger seems to work, and I cannot create duplicates from the website, yet every now and then they do show up.

 

Is there anything I can do at the trigger end, to get a better handle on these dupes?

sfdcfoxsfdcfox

If you're using the standard web to lead form, and the user submits more than one record at a time (e.g. double-clicking the submit button), those items may be queued together into the same trigger invocation. This means that a proper trigger should also check incoming records against each other as well to verify that there aren't in-flight dupes. You can do this by adding the values to a set. Here's one possible version:

 

trigger X on Contact (before insert) {
	Map<String, Map<String, Contact>> contacts = new Map<String, Map<String, Contact>>();
	// Step 1: Map in-flight contacts, prevents in-flight duplicates
	for(Contact record: Trigger.new) {
		String escapedSource = String.escapeSingleQuotes(record.leadSource),
			   escapedEmail = String.escapeSingleQuotes(record.email);
		if(!contacts.containsKey(escapedSource)) {
			contacts.put(escapedSource, new Map<String, Contact>());
		}
		if(contacts.get(escapedSource).containsKey(escapedEmail)) {
			record.addError('Duplicate Lead detected.');
		} else {
			contacts.get(escapedSource).put(escapedEmail, record);
		}
	}
	// Step 2: Prepare query to check for duplicates
	String[] queryFilters = new String[0];
	for(String source: contacts.keySet()) {
		queryFilters.add(
			String.format('(LeadSource = '{0}' AND Email IN (\'\'{1}\'\')',
				new String[] {
					source,
					String.join(new List<String>(contacts.get(source).keySet()),'\',\'')
				}
			)
		);
	}
	// Query should look like:
	// SELECT Id, Email, LeadSource FROM Contact WHERE CreatedDate = TODAY AND
	//	(LeadSource = 'Source1' AND Email IN ('email1','email2') OR (LeadSource = 'Source2' AND Email IN ('email3','email4')))
	
	// Step 3: Query existing records, mark in-flight records as duplicates.
	for(Contact record:Database.query('SELECT Id, Email, LeadSource FROM Contact WHERE CreatedDate = TODAY AND ('+String.join(queryFilters,' OR ')+')')) {
		contacts.get(String.escapeSingleQuotes(record.LeadSource)).get(String.escapeSingleQuotes(record.Email)).addError('Duplicate Lead detected');
	}
}

 

This was selected as the best answer
Forza di SognoForza di Sogno

SFDCFox, thanks for the code. I will have to study it and figure out what it's doing under the hood.

 

One problem I have now is that my test class no longer works/passes.  Would you take a look at it and SFDCFoxify it?

 

Thank you...

 

@istest
class AvoidWebleadDuplicatesTrigger_Test
{
    static testmethod void test()
    {
        //Create the conditions to meet the steps taken by your class
        //Create 4 contacts (2 unique ones and 2 duplicates)

        Account a = new Account(name='Test');
        insert a;
       
        List<Contact> contacts = new List<Contact>();
       
        Contact c1 = new Contact (FirstName = 'joe', LastName='smith1', Email = 'joesmith1@gmail.com', LeadSource = 'Landing Page Website', AccountId = a.Id);
        Contact c2 = new Contact (FirstName = 'joe', LastName='smith2', Email = 'joesmith2@gmail.com', LeadSource = 'Landing Page Test', AccountId = a.Id);
        Contact c3 = new Contact (FirstName = 'joe', LastName='smith1', Email = 'joesmith1@gmail.com', LeadSource = 'Landing Page Website', AccountId = a.Id);
        Contact c4 = new Contact (FirstName = 'joe', LastName='smith2', Email = 'joesmith2@gmail.com', LeadSource = 'Landing Page Test', AccountId = a.Id);       

        contacts.add(c1);
        contacts.add(c2);
        contacts.add(c3);
        contacts.add(c4);
       
        insert contacts;
   
    }
}

 

sfdcfoxsfdcfox

Your test method is actually attempting to insert duplicates, and the trigger is now correctly blocking those duplicates (it wasn't before).

 

The only change you need here is to verify the trigger is operating as intended:

 

 @istest
class AvoidWebleadDuplicatesTrigger_Test
{
    static testmethod void test()
    {
        //Create the conditions to meet the steps taken by your class
        //Create 4 contacts (2 unique ones and 2 duplicates)

        Account a = new Account(name='Test');
        insert a;
       
        List<Contact> contacts = new List<Contact>();
       
        Contact c1 = new Contact (FirstName = 'joe', LastName='smith1', Email = 'joesmith1@gmail.com', LeadSource = 'Landing Page Website', AccountId = a.Id);
        Contact c2 = new Contact (FirstName = 'joe', LastName='smith2', Email = 'joesmith2@gmail.com', LeadSource = 'Landing Page Test', AccountId = a.Id);
        Contact c3 = new Contact (FirstName = 'joe', LastName='smith1', Email = 'joesmith1@gmail.com', LeadSource = 'Landing Page Website', AccountId = a.Id);
        Contact c4 = new Contact (FirstName = 'joe', LastName='smith2', Email = 'joesmith2@gmail.com', LeadSource = 'Landing Page Test', AccountId = a.Id);       

        contacts.add(c1);
        contacts.add(c2);
        contacts.add(c3);
        contacts.add(c4);
       
        // Allow a partial save
        Database.SaveResult[] results = Database.insert(contacts, false);
        // Assert that duplicates were blocked
        System.assert(results[0].isSuccess());
        System.assert(results[1].isSuccess());
        System.assert(!results[2].isSuccess());
        System.assert(!results[3].isSuccess());
    }
}

 

Forza di SognoForza di Sogno

I thought the point of the test class was to recreate the scenario that I'm trying to test for.  That is the reason I'm creating 2 regular contacts and 2 duplicate contacts.  Is that not the correct approach?

 

On the test class side, for the 4 system.asserts I got false, false, true, true.  That means the first 2 inserts failed, and the second 2 (the dupes) succeeded to fail (as expected).  What could have caused the non-dupes to fail?

 

There are some workflows running in the background, but I thought the before trigger would execute before any workflows, so that shouldn't be an impact.  *Shouldn't*...

sfdcfoxsfdcfox
The purpose of unit testing is to verify the logical correctness of code that compiles. This automated testing is intended to help developers find logic flaws in the code that would otherwise go unnoticed, a check and balance system. In this case, the purpose of the testing is to assert that the logic will not allow duplicates and allow non-duplicates.

The test method you proffered initially was correct in its intent, but the logic bug in the method (not verifying the success and failure of each record), hid the actual bug in the trigger logic. When the logic bug of the trigger was fixed, it exposed the bug in the test method, which also needed to be fixed.

Triggers do execute before workflow rules, but workflow rules can also cause an echo effect that causes triggers that executed once to execute a second time. This gives the trigger an opportunity to respond to changes to the record that occurred as a result of a field update. However, that should not have an effect here, because the records are still isolated, and so should not have failed.

You can check the results of each Database.SaveResult in a loop, using System.Debug to dump out their success/failure flags and any error messages, then check the log file after running the test (located in the Developer Console). Once you determine the error, you can take steps to correct it.

There are several types of errors that could prevent saving, including validation rules, other triggers on the same object failing, required fields missing (universally required permission), invalid record types selected for the test user, or any of a dozen other reasons (or more).

Without the log files, it will be difficult to know why the tests failed or what steps you can take to fix the test. That is your next logical step.
Forza di SognoForza di Sogno

SFDCFox, I'm not able to paste the log file here, even a shortened version, so I'm just adding the errors I spotted:

 

ne 1:87 no viable alternative at character '"'
07:19:05.669 (669544000)|FATAL_ERROR|System.QueryException: line 1:87 no viable alternative at character '"'

Trigger.AvoidWebleadDuplicatesTrigger: line 40, column 1
07:19:05.669 (669561000)|FATAL_ERROR|System.QueryException: line 1:87 no viable alternative at character '"'

Trigger.AvoidWebleadDuplicatesTrigger: line 40, column 1

Line 40 of the trigger deals with building a SQL query:

 

    for(Contact record:Database.query('SELECT Id, Email, LeadSource FROM Contact WHERE CreatedDate = TODAY AND ('+String.join(queryFilters,' OR ')+')'))
    {
        contacts.get(String.escapeSingleQuotes(record.LeadSource)).get(String.escapeSingleQuotes(record.Email)).addError('This contact already exists. (step 3)');
    }

sfdcfoxsfdcfox

Sorry, I wrote the prior code without testing. Here's a tested version:

 

trigger X on Contact (before insert) {
    Map<String, Map<String, Contact>> contacts = new Map<String, Map<String, Contact>>();
    // Step 1: Map in-flight contacts, prevents in-flight duplicates
    for(Contact record: Trigger.new) {
        String escapedSource = String.isBlank(record.LeadSource)?'null':'\''+String.escapeSingleQuotes(record.leadSource)+'\'',
               escapedEmail = String.isBlank(record.Email)?'null':'\''+String.escapeSingleQuotes(record.email)+'\'';
        if(!contacts.containsKey(escapedSource)) {
            contacts.put(escapedSource, new Map<String, Contact>());
        }
        if(contacts.get(escapedSource).containsKey(escapedEmail)) {
            record.addError('Duplicate Lead detected.');
        } else {
            contacts.get(escapedSource).put(escapedEmail, record);
        }
    }
    // Step 2: Prepare query to check for duplicates
    String[] queryFilters = new String[0];
    for(String source: contacts.keySet()) {
        queryFilters.add(
            String.format('(LeadSource = {0} AND Email IN ({1}))',
                new String[] {
                    source,
                    String.join(new List<String>(contacts.get(source).keySet()),'\',\'')
                }
            )
        );
    }
    // Query should look like:
    // SELECT Id, Email, LeadSource FROM Contact WHERE CreatedDate = TODAY AND
    //  (LeadSource = 'Source1' AND Email IN ('email1','email2') OR (LeadSource = 'Source2' AND Email IN ('email3','email4')))
    
    // Step 3: Query existing records, mark in-flight records as duplicates.
    String query = 'SELECT Id, Email, LeadSource FROM Contact WHERE CreatedDate = TODAY AND ('+String.join(queryFilters,' OR ')+')';
    for(Contact record:Database.query(query)) {
        String escapedSource = String.isBlank(record.LeadSource)?'null':'\''+String.escapeSingleQuotes(record.leadSource)+'\'',
               escapedEmail = String.isBlank(record.Email)?'null':'\''+String.escapeSingleQuotes(record.email)+'\'';    
        contacts.get(escapedSource).get(escapedEmail).addError('Duplicate Lead detected');
    }
}

 

Forza di SognoForza di Sogno

Thank you very much - you really provided a great service!  I've learned a lot and will probably learn some more when I study your latest code.