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
toddnewmantoddnewman 

Salesforce-2-Salesforce and Lead Conversion

===================================================

S2S does not automatically keep relationships intact. There is a workaround posted here but I couldn't get it to work for lead conversion.

 

The problem with the above workaround is that account synchronization needs to be fully established before we can get the information we need to set up the relationships in the client org. Since account/contact/opportunities are created almost simultaneously on conversion that is no good. I tried several variations with code in one org or the other to no avail. Those three objects are simply created more quickly than the synch is completed. So I resorted to scheduled Apex.

 

Using scheduled Apex from a trigger has some gotchas. Job names must be unique and you can only have 20 scheduled jobs at one time. Not to mention you can't query the status of jobs by ID or name.

 

I use a custom object to keep track of which records still need to be synchronized and which jobs they are attached to (within some hack-y reason). You could use custom fields instead but I think the custom object is a little cleaner.

 

This is not fully debugged and the error handling is incomplete. I noticed that if I convert a second lead before the first one is done, I get two scheduled jobs. But that does not lead to an error so I let it be.

 

Hope this helps some folks out there.

 

 

================== Custom Object ==================

S2SParentPropogation__c

JobID__c, Text(18)

LocalAccountId__c, Text(18)

LocalId__c, Text(18)

Status__c, Picklist (values of Inserted, Completed, Error)

===================================================

 

================= Contact Trigger =================

trigger Contact_AfterInsert on Contact (after insert) {

 

  S2S_EstablishPropogation prop = new S2S_EstablishPropogation();

  prop.createContactSyncJob(Trigger.new);

 

}

===================================================

 

================= Opportunity Trigger =================

trigger Opportunity_AfterInsert on Opportunity (after insert) {

 

  S2S_EstablishPropogation prop = new S2S_EstablishPropogation();

  prop.createOpportunitySyncJob(Trigger.new);

 

}

=======================================================

 

============ S2S_EstablishPropogation Class ============

public class S2S_EstablishPropogation {

 

  public void createContactSyncJob(List<Contact> contacts) {

 

    // init

    List<PartnerNetworkRecordConnection> newSynchRecords = new List<PartnerNetworkRecordConnection>();

    List<Contact> toSynchLater = new List<Contact>();

    S2S_Utils su = new S2S_Utils();

 

    // build list of account IDs

    List<ID> localAccountIds = new List<ID>();

    for (Contact c : contacts) {

      localAccountIds.add(c.AccountId);

    }

 

    // figure out which of the parent accounts have been synced already

    List<PartnerNetworkRecordConnection> findSynchedAccounts = su.findAlreadySynchedAccounts(localAccountIds);

    Set<ID> synchedLocalAccounts = buildSetOfLocalAccountIds(findSynchedAccounts);

 

    // process those contacts who's accounts have been synced and store the rest for later

    for (Contact c : contacts) {

      // if the parent account has already been synched, we can save ourself some work later

      if (synchedLocalAccounts.contains(c.AccountId)) {

        // connection record will auto-share this contact with the client org

        newSynchRecords.add(su.buildPartnerConnectionRecord(c.Id, c.AccountId));

 

      // no joy, log this to a table to synch later

      } else { toSynchLater.add(c); }

    }

    // insert those we can now

    insert newSynchRecords;

 

    // now create records for later syncing

    if (toSynchLater.size() > 0) {

      // make sure there's a job in the future to process the records we're about to create

      String jobId = findOrCreateJobThatEstablishesSharesLater('003');

 

      // finally, create record of opportunities to sync

      List<S2SParentPropogation__c> deferredSyncRecords = new List<S2SParentPropogation__c>();

      for (Contact c : toSynchLater) {

        deferredSyncRecords.add(createSyncLaterRecord(c.Id, c.AccountId, jobId));

      }

      insert deferredSyncRecords;

    }

 

  }

 

  public void createOpportunitySyncJob(List<Opportunity> oppts) {

 

    // init

    List<PartnerNetworkRecordConnection> newSynchRecords = new List<PartnerNetworkRecordConnection>();

    List<Opportunity> toSynchLater = new List<Opportunity>();

    S2S_Utils su = new S2S_Utils();

 

    // build list of account IDs

    List<ID> localAccountIds = new List<ID>();

    for (Opportunity o : oppts) {

      localAccountIds.add(o.AccountId);

    }

 

    // figure out which of the parent accounts have been synced already

    List<PartnerNetworkRecordConnection> findSynchedAccounts = su.findAlreadySynchedAccounts(localAccountIds);

    Set<ID> synchedLocalAccounts = buildSetOfLocalAccountIds(findSynchedAccounts);

 

    // process those opporunities who's accounts have been synced and store the rest for later

    for (Opportunity o : oppts) {

      // if the parent account has already been synched, we can save ourself some work later

      if (synchedLocalAccounts.contains(o.AccountId)) {

        // connection record will auto-share this oppt with the client org

        newSynchRecords.add(su.buildPartnerConnectionRecord(o.Id, o.AccountId));

 

      // no joy, log this to a table to synch later

      } else { toSynchLater.add(o); }

    }

    // insert those we can now

    insert newSynchRecords;

 

    // now create records for later syncing

    if (toSynchLater.size() > 0) {

      // make sure there's a job in the future to process the records we're about to create

      String jobId = findOrCreateJobThatEstablishesSharesLater('006');

 

      // create record of opportunities to sync

      List<S2SParentPropogation__c> deferredSyncRecords = new List<S2SParentPropogation__c>();

      for (Opportunity o : toSynchLater) {

        deferredSyncRecords.add(createSyncLaterRecord(o.Id, o.AccountId, jobId));

      }

      insert deferredSyncRecords;

    }

 

  }

 

  private String scheduleJob(String objectType) {

    Integer rand = Crypto.getRandomInteger();

    String ran = String.valueOf(rand).substring(5);

    S2S_Utils su = new S2S_Utils();

    String sch = su.getScheduleStringMinutesFromNow(S2S_Utils.SCHEDULE_JOB_HOW_MANY_MINUTES_IN_FUTURE);

    S2S_PropogateParent prop = new S2S_PropogateParent();

    return System.schedule('S2S sync-' + objectType + '-' + ran, sch, prop);

  }

 

  private Set<ID> buildSetOfLocalAccountIds(List<PartnerNetworkRecordConnection> findSynchedAccounts) {

    Set<ID> synchedLocalAccounts = new Set<ID>();

    for (PartnerNetworkRecordConnection a : findSynchedAccounts) {

      synchedLocalAccounts.add(a.LocalRecordId);

    }

    return synchedLocalAccounts;

  }

 

  private String findOrCreateJobThatEstablishesSharesLater(String objectType) {

 

    String jobId = 'addon';

 

    // find already scehduled jobs

    // we can't query directly so we have to make use of our own stored IDs

    Datetime wasCreatedAtOrAfter = Datetime.now().addSeconds(30).addMinutes(-3);

    List<S2SParentPropogation__c> existingJobs =

      [SELECT Id FROM S2SParentPropogation__c

      WHERE JobID__c != null

      AND JobID__c != 'addon'

      AND Status__c = 'Inserted'

      AND CreatedDate > :wasCreatedAtOrAfter];

 

    if (existingJobs.size() == 0) {

      // nothing is scheduled, create a job

      jobId = scheduleJob(objectType);

    }

 

    return jobId;

 

  }

 

  private S2SParentPropogation__c createSyncLaterRecord(ID child, ID parent, String jobId) {

    return new S2SParentPropogation__c(

      LocalId__c = child,

      LocalAccountId__c = parent,

      Status__c = 'Inserted',

      JobID__c = jobId

    );

  }

 

}

====================================================

 

============ S2S_PropogateParent Class ============

global class S2S_PropogateParent implements Schedulable {

 

  global void execute(SchedulableContext SC) {

 

    S2S_Utils su = new S2S_Utils();

 

    // clear out previously completed records

    Datetime hourAgo = DateTime.now().addHours(-1);

    List<S2SParentPropogation__c> deleteNow = [SELECT Id FROM S2SParentPropogation__c WHERE Status__c = 'Completed' AND CreatedDate < :hourAgo];

    try {

      delete deleteNow;

    } catch (Exception e) {

      // meh, it was probably already deleted by another job running in parallel

    }

 

    /****************** Retrieve Data **********************/

    // what records are out there awaiting sync

    Datetime minsAgo = DateTime.now().addMinutes(-1);

    List<S2SParentPropogation__c> processNow =

      [SELECT LocalId__c, LocalAccountId__c, Status__c

      FROM S2SParentPropogation__c

      WHERE Status__c = 'Inserted'

      AND (JobID__c = 'DOH-testing' OR CreatedDate < :minsAgo)];

 

    // get list of accounts so we know which sync records to look for

    List<ID> localAccountIds = new List<ID>();

    for (S2SParentPropogation__c pp : processNow) {

      localAccountIds.add(pp.LocalAccountId__c);

    }

 

    // which of those accounts have completed sync?

    List<PartnerNetworkRecordConnection> findSynchedAccounts = su.findAlreadySynchedAccounts(localAccountIds);

 

    /****************** Add Sync Records **********************/

    List<PartnerNetworkRecordConnection> newSynchRecords = new List<PartnerNetworkRecordConnection>();

    for (S2SParentPropogation__c p : processNow) {

 

      // find corresponding account S2S sync record

      PartnerNetworkRecordConnection accountS2S = null;

      for (PartnerNetworkRecordConnection pn : findSynchedAccounts) {

        if (pn.LocalRecordId == p.localAccountId__c) {

          accountS2S = pn;

        }

      }

 

      // found it

      if (accountS2S != null) {

        // auto-share this contact/opportunity with the client org

        newSynchRecords.add(su.buildPartnerConnectionRecord(p.LocalId__c, p.LocalAccountId__c));

        p.Status__c = 'Completed';

      }

    }

 

    insert newSynchRecords;

    update processNow;

 

  }

 

}

===================================================

 

================= S2S_Utils Class =================

public class S2S_Utils {

 

  public static Integer SCHEDULE_JOB_HOW_MANY_MINUTES_IN_FUTURE = 3;

  public static String CONNECTION_NAME = '[Connection Name]';

 

  public List<PartnerNetworkRecordConnection> findAlreadySynchedAccounts(List<ID> localAccountIds) {

    // find accounts that have already been synched

    return [SELECT Id, ConnectionId, PartnerRecordId, LocalRecordId

      FROM PartnerNetworkRecordConnection

      WHERE LocalRecordId in :localAccountIds

      AND (Status ='Sent' OR Status = 'Received')];

  }

 

  public PartnerNetworkRecordConnection buildPartnerConnectionRecord(ID child, ID parent) {

    return new PartnerNetworkRecordConnection (

      ConnectionId = getConnectionId(),

      LocalRecordId = child,

      ParentRecordId = parent,

      SendClosedTasks = false,

      SendOpenTasks = false,

      SendEmails = false

    );

  }

 

  public ID getConnectionId() {

    List<PartnerNetworkConnection> partnerNetConList =

      [SELECT id FROM PartnerNetworkConnection

      WHERE connectionStatus = 'Accepted'

      AND connectionName = :S2S_Utils.CONNECTION_NAME];

 

    if (partnerNetConList.size() != 0) {

      return partnerNetConList.get(0).Id;

    }

 

    return null;

  }

 

  public String getScheduleStringMinutesFromNow(Integer mins) {

    Datetime runTime = Datetime.now().addMinutes(mins);

    return twoDigit(runTime.second()) + ' ' + twoDigit(runTime.minute()) + ' ' + twoDigit(runTime.hour()) + ' ' + twoDigit(runTime.day()) + ' ' + twoDigit(runTime.month()) + ' ? ' + runTime.year();

  }

 

  public String twoDigit(integer raw) {

    if (raw < 10 && raw > 0) {

      return '0' + String.valueOf(raw);

    } else {

      return String.valueOf(raw);

    }

  }

 

}

SF7SF7
HI ,
I know this is a very old post , i am running into same issue what you faced and i am trying to implement this . have you had success with this? And how does the scheduling work , after i schedle the batch the Converted leads records are not going to the target Org

thanks
Akhil