You need to sign in to do that
Don't have an account?
New exception with Winter '10 when trying to update CampaignMember records
I have a trigger on Opportunity that either creates a campaign membership for the contact that is the primary contact role on an opportunity, or updates the campaign membership to a campaign member status of 'Donated' when the opportunity IsWon.
Before the Summer '09 release, I could just create a new campaign member for a contact and it would overwrite the status. With Summer '09 however, Salesforce disallowed the creation of a new campaign member record (if one already existed) and I was getting exceptions that a duplicate campaign member record exists already.
So I refactored, adding a SOQL to find out if the CM existed and then just update the status.
My trigger just starting failing with the Winter '10 release. Specifically, I have a custom lead converter, and a test that is testing whether an opp gets created on lead conversion. But the opp isn't getting created because when the lead converter test runs, my Opportunity trigger fires, tries to update the Campaign Member, and throws an exception telling me that I can't update a converted lead. I don't have any DML's related to lead on my Opportunity trigger, all I'm trying to do is update CampaignMember records.
Please advise, I suspect a bug here related to some of the new CampaignMember functionality added with Winter' 10. I'm happy to provide code if that's helpful.
It sounds like upon lead convert you aren't grabbing the resulting Contact ID instead of of Lead ID, causing exception due to the fact that converted leads can't be changed (or referenced for creating new campaign members).
I'd throw a check for whether the lead is converted in your CM create code, and if so, grab the contact ID from that converted lead record, to be used for creating the CM.
Hi John,
Just to clarify, we're not creating new CM's on lead conversion -- these are leads with existing campaign memberships. The custom lead converter uses the standard leadConvert method, but then adds a few additional functions (like creating Opportunities).
All we're doing on lead conversion is updating the CM record to change the member status (handled by a separate trigger actually, not the controller and class of the custom lead converter), my code does not touch the contact or lead IDs of the campaign member record.
I would think that updating the status of the campaign member record should have absolutely nothing to do with whether or not the lead is converted or not. Again, the trigger that updates the CMs does not touch lead or contact at all, the only DML is on CampaignMember.
Note if you're using API v15.0 and below, campaign member insert & update still work as upsert, as they used to. The new Upsert behavior is a bit different than the old.
If that doesn't solve your problem, log a case w/ support to dive into the details.
Hi John,
This problem is not resolved yet, although my case has been escalated.
I'm getting down to the root of the matter though. I've inserted a bunch ofdebug statements and here is the important one:
CMDebug is my debug statement and it's displaying a list of Campaign members to update, in this case, there is only one. Note that this Campaign Member was created and intially associated with a lead, the lead was then converted using the Database.LeadConvert method, and the CM now has a ContactId. When I attempt the update DML on the CampaignMember list, I get an exception that I cannot update a converted lead.This error makes no sense to me, as this CampaignMember clearly has a ContactId already. Please advise.
Here's my trigger:
trigger ONEN_Opportunity_AddToCampaign on Opportunity (after insert, after update) {
// when we first end up w/ a closedwon Opp that has a campaign source,
// ensure that the primary contact has a campaign mbrship for that campaign set to Responded
// don't worry about opp deletions - don't ever remove camp mbrship once created
// note: this doesn't prevent user from later removing camp mbr or changing it to not responded - that's OK I think
// also note: won't trigger if user later changes primary contact on opp - that's probably OK too
//system.debug ( 'TRIGGER FIRED');
string DefaultDonatedStatus = ONEN_Constants.CAMPAIGN_DEFAULT_DONATED_STATUS;
Set<id> allOppIds = new Set<id> ();
Map<id,id> OppToContactIds = new Map<id,id> ();
// Map<id,Map<id,CampaignMember>> ContactToCMs = new Map<id,Map<id,CampaignMember>> ();
Map<id,Opportunity> oppsToProcess = new Map<id,Opportunity> ();
List<CampaignMember> CMsToInsert = new List<CampaignMember> ();
List<CampaignMember> CMsToUpdate = new List<CampaignMember> ();
// this map's keys will be a concatenation of contactID & campaignID
Map<string,CampaignMember> comboIDMap = new Map<string,CampaignMember> ();
// use a set for this because it will automatically dedup
Set<id> CampaignsToCheckCMS = new Set<id> ();
Set<id> CampToCheckResponded = new Set<id> ();
Set<id> CMIDsToUpdateSet = new set<id> ();
Set<id> CampaignIds = new set<id> ();
set<id> nongiftRTids = ONEN_RecTypes.GetRecordTypeIdSet('Opportunity',ONEN_Constants.OPP_RECTYPES_NOT_GIFT);
// first, make sure we have checked for contact role creation if necessary
// (have to do this here to insure triggers happen in the right order)
if ( Trigger.isInsert && ONEN_OpportunityContactRoles.haveCheckedContactRoles == false ) {
ONEN_OpportunityContactRoles.CheckContactRoles ( trigger.newmap );
}
// first figure out which opps need processing - only want contact opps whose
for ( Opportunity opp : Trigger.new ) {
If ( opp.CampaignID != Null && opp.AccountID==null &&
(Trigger.IsInsert ||
Trigger.Old[0].CampaignID != opp.CampaignID ||
Trigger.Old[0].StageName != opp.StageName )) {
oppsToProcess.put (opp.id, opp);
CampaignIds.add(opp.CampaignId);
}
}
If ( oppsToProcess.size() > 0 ) {
// * get set of oppID's in trigger.new
// and pass set to OppContactRoles.getPrimaryContactID, returns map PrimaryContacts (OppID -> ContactID) allOppIds = oppsToProcess.keySet();
OppToContactIds = ONEN_OpportunityContactRoles.GetPrimaryContactIdsBulk (allOppIds);
//system.debug ('oppToContactIds map: ' + OppToContactIds);
// map combos of contact & camp ID's to the CM's
for (CampaignMember thisCM : [Select Id, ContactId, LeadId, Lead.IsConverted, CampaignId, Status, HasResponded From CampaignMember WHERE ContactId IN :OppToContactIds.values() AND CampaignId IN :CampaignIds]) {
string comboID = thisCM.ContactID + '|' + thisCM.CampaignID;
comboIDMap.put (comboID,thisCM);
}
system.debug('This CampaignMember: ' + comboIDMap);
// now loop through all of our opps
for (Opportunity thisOpp : oppsToProcess.values() ) {
id ConId = OppToContactIds.get(thisOpp.Id);
id CampId = thisOpp.CampaignId;
// but only process those that actually have a primary contact
if ( ConId != null) {
CampaignMember thisCM;
string comboID = ConId + '|' + CampId;
thisCM = comboIDMap.get(comboID);
// if this contact doesn't already have a CM for this campaign, create one
if ( thisCM == null ) {
CampaignMember newCM = new CampaignMember(ContactId = ConId, CampaignId = CampId);
//use the default status unless the opp is won, then use the default donated status if it's a gift
if (thisOpp.IsWon == True) {
If ( nongiftRTids.contains(thisOpp.RecordTypeId) == true) {
newCM.Status = 'Responded';
CampToCheckResponded.add (CampId);
} else {
newCM.Status = DefaultDonatedStatus;
CampaignsToCheckCMS.add (CampId);
}
}
CMsToInsert.add (newCM);
comboIDMap.put (comboID,newCM);
} else {
// if they do already have a CM, chg the status where appropriate
if ( thisCM.HasResponded == true || thisOpp.IsWon == false) {
// if already responded, or not won, do nothing
} else {
If ( nongiftRTids.contains(thisOpp.RecordTypeId) == true) {
thisCM.Status = 'Responded';
CampToCheckResponded.add (CampId);
} else {
thisCM.Status = DefaultDonatedStatus;
CampaignsToCheckCMS.add (CampId);
}
if (CMIDsToUpdateSet.add(thisCM.Id)) {
CMsToUpdate.add(thisCM);
}
}
}
}
// check list of Campaigns that they have the Donated status, add if not
// last param means we require that the status be HasResponded, and change it to that if it's not
// already
boolean statusOK = ONEN_CampaignMemberStatus.CheckCMStatusExistsBulk (CampaignsToCheckCMS,DefaultDonatedStatus,true);
boolean statusOK2 = ONEN_CampaignMemberStatus.CheckCMStatusExistsBulk (CampToCheckResponded,'Responded',true);
//system.debug ('to update: ' + CMsToUpdate);
//system.debug ('to insert: ' + CMsToInsert);
// need to insert the new ones first, in case any will be subsequent dups that need updating.
if ( CMsToInsert.size() > 0 ) {
insert CMsToInsert;
}
system.debug('CMDebug: ' + CMsToUpdate);
if ( CMsToUpdate.size() > 0 ) {
update CMsToUpdate;
}
}
}
Sounds like these 2 lines are causing the problem:
CampaignMember newCM = new CampaignMember(ContactId = ConId, CampaignId = CampId);
...
Insert CMsToInsert;
I haven't done so, but can you create a simple trigger, hard code a campaign ID and a contact ID that was from a converted lead, and try to insert a campaign member using these 2 lines? If that error comes up, might be a bug for my team to look at.
I'd check myself but I'm smashed w/ Dreamforce right now.
Hi John,
I'm back from Dreamforce and just following up on this thread. Sorry I didn't get a chance to meet you.
On your suggestion, I wrote an very simple trigger and test to confirm the bug. The trigger is on contact, and it fires off an update to a campaign member record of a converted lead. Here is the trigger code:
trigger Test_Trigger on Contact (after insert, after update) { CampaignMember cm = new CampaignMember();
if (trigger.new[0].MailingCity == 'TOTALLY_BOGUS') { cm = [select id, status from CampaignMember where ContactId = :trigger.new[0].id LIMIT 1]; cm.status = 'Donated'; update cm;
}
}
And here is the test:
@isTest
private class TEST_TestTrigger {
static testMethod void myUnitTest() { // status label is needed for conversion
LeadStatus convertStatus = [Select MasterLabel from LeadStatus where IsConverted=true order by MasterLabel limit 1];
Account individual = [select id from Account where Name = 'Individual' LIMIT 1];
Database.LeadConvert lc = new Database.LeadConvert();
Lead l = new Lead(
LastName = 'Bogus',
Company = '[not provided]'
);
insert l;
Campaign camp = new Campaign(
Name = 'New BOGUS Campaign'
);
insert camp;
CampaignMember member = new CampaignMember(
LeadId = l.id,
CampaignId = camp.id,
Status = 'Sent'
);
insert member;
lc.setLeadId(l.id);
lc.setAccountId(individual.id);
lc.setConvertedStatus(convertStatus.MasterLabel);
Database.leadConvertResult lcr = Database.convertLead(lc, false);
system.assert(lcr.isSuccess());
Contact con = [select id, MailingCity from Contact WHERE id = :lcr.getContactId() LIMIT 1];
con.MailingCity = 'TOTALLY_BOGUS';
update con;
}
}
I created a case in October and it's been escalated but it hasn't seen any traction in a couple of weeks.
Case Number: 03040305
Any status update that you could provide would be much appreciated. Thanks!