Salesforce

Code Snippet: Mass Converting Person Accounts

Converting thousands of Contacts into Person Accounts can be a critical step for organizations that need to take advantage of more advanced Salesforce functionality. Many Salesforce products, such as Health Cloud and Financial Services Cloud, require the use of Person Accounts.  (Previously they were optional.)

Person Accounts are hybrid records that combine the details of both Accounts and Contacts.  However, the conversion process can be complex. There are often dozens of related records (Calls, Emails, Tasks, Files, Opportunities, etc.) related to each Contact. Without a thoughtful conversion plan, those related records may be attached to the wrong record after the conversion.  Or the conversation may fail with difficult to decipher errors.

Why You Might Need a Custom Apex Approach

Salesforce provides standard tools and documentation for creating Person Accounts, but those tools are typically intended for small-scale conversions. When your organization has thousands of Contacts to convert, particularly if the conversion needs to be staged in waves, using Apex becomes a more manageable path. A custom process gives you control over the order of records, error handling, and how related records are re-parented as part of the conversion.

High-Level Approach

Converting standard Contacts to Person Accounts is primarily a data preparation exercise. Salesforce provides technical documentation outlining the required field conditions; however, those conditions must be evaluated and addressed in advance. In general, you should:

  • Review the official Salesforce Person Account implementation documentation.
  • Prepare a staging report (or data export) showing which Contact records are in scope.
  • Use Apex to evaluate and clean up related records in batches.
  • Insert or update records in a specific order to avoid locking errors or cascade failures.

Required Data Conditions Before Conversion

Successful conversion requires the source Account / Contact data to meet a very specific set of requirements:

  • Single Contact: Each Account intended to become a Person Account must have one (and only one) related Contact.
  • Matching Owners: The Account and its single Contact must have the same OwnerId value.
  • Matching Currency: If multi-currency is enabled, both records must have the same currency code.
  • No Parent Relationship: The Parent Account field on the Account and the Reports To field on the Contact must be blank.
  • Account Not Parent: The Account cannot be used as the Parent for any other Accounts.
  • Contact Not Reporting: The Contact cannot be listed as the Reports To for any other Contact.
  • Not Enabled for Portal / Community: Neither the Account nor the Contact can be enabled for Partner or Community use.

These constraints are enforced at the database level. If even one of these conditions is not met, the conversion will fail.

Approach Details (Suggested Steps)

  1. Export a list of qualifying Accounts and Contacts for review
  2. Validate each of the above conditions programmatically
  3. Temporarily re-parent or archive non-matching related records (as needed)
  4. Use an Apex Batch to loop through each qualifying record in sets of 100
  5. Perform the conversion and reconnect Tasks, Events, Emails, Files, etc.

All of this should be staged in a sandbox first. It may take several rounds of incremental cleanup before records are eligible for conversion.

Example Apex Conversion Code

Below is an example of an apex batch we successfully used to convert 50k contacts and household accounts into Person Accounts.  The approach may need to be altered for your org’s specific requirements.  For example, you may not want to keep the household accounts (families), or you may want to migrate the household relationships to Account-Contact relationships.

 

Note: this is pseudocode for explanation purposes, not a production-ready migration script.

public class PersAcctConverterBatch implements Database.Batchable<SObject> {

    public Database.QueryLocator start(Database.BatchableContext bc) {
        return Database.getQueryLocator([
            SELECT Id, Name, FirstName, LastName, AccountId, Account.Family_Account__c, OwnerId 
            FROM Contact 
            WHERE Account.IsPersonAccount = false
        ]);
    }

    public void execute(Database.BatchableContext bc, List<Contact> scope) {
        Id personRtId = Schema.SObjectType.Account.getRecordTypeInfosByDeveloperName().get('PersonAccount').getRecordTypeId();

        // Map ContactId -> original household AccountId
        Map<Id, Id> contactIdToHouseholdId = new Map<Id, Id>();
        for (Contact c : scope){
           contactIdToHouseholdId.put(c.Id, c.AccountId); 
        } 

        // Build new Person Accounts, one per Contact; use Contact.Name as the Account Name
        List<Account> newPersonShellAccounts = new List<Account>();
        
        // Track ContactId -> index in new accounts list to reconcile Ids later
        Map<Id, Integer> contactIndexMap = new Map<Id, Integer>();
        Integer idx = 0;
        for (Contact c : scope) {
            Account newPersAccount = new Account();
            newPersAccount.Name = c.Name; 
            //Don't use firstname/lastname here or it's automatically a person account with new contact.
            //Make other record edits here as needed.
            newPersAccount.OwnerId = c.OwnerId; 
            //if owners don't match, the convert fails on the last step.
            newPersonShellAccounts.add(newPersAccount);
            contactIndexMap.put(c.Id, idx);
            idx++;
        }
        
        // 1) Insert new placeholder Accounts
        Database.SaveResult[] acctInsertResults = Database.insert(newPersonShellAccounts, false);

        // Build Contact updates and a set of new Account Ids that succeeded
        List<Contact> contactsToUpdate = new List<Contact>();
        Set<Id> newlyInsertedAccountIds = new Set<Id>();
        Map<Id, Id> contactIdToNewAccountId = new Map<Id, Id>();

        for (Integer i = 0; i < acctInsertResults.size(); i++) {
            Database.SaveResult sr = acctInsertResults[i];
            if (sr.isSuccess()) {
                newlyInsertedAccountIds.add(newPersonShellAccounts[i].Id);
            }
        }
        // Build a reverse map from index -> ContactId to map the IDs 
        //(omitted for brevity)

        // Now map Contact -> New Account
        for (Integer i = 0; i < newPersonShellAccounts.size(); i++) {
            if (i < acctInsertResults.size() && acctInsertResults[i].isSuccess()) {
                Id cId = indexToContactId.get(i);
                Id newAcctId = newPersonShellAccounts[i].Id;
                contactIdToNewAccountId.put(cId, newAcctId);
            }
        }

        // Prepare Contact updates
        for (Contact c : scope) {
            Id newAcctId = contactIdToNewAccountId.get(c.Id);
            if (newAcctId == null) continue; // skip failed account insert
            Contact contToUpdate = new Contact(Id = c.Id, AccountId = newAcctId);
            contactsToUpdate.add(contToUpdate);
        }

        if (!contactsToUpdate.isEmpty()) {
            Database.update(contactsToUpdate, false);
        }

        //Convert the newly created Accounts to Person Accounts by setting RecordTypeId
        //Only attempt on the accounts that actually inserted
        List<Account> accountsToConvert = new List<Account>();
        for (Account a : newPersonShellAccounts) {
            if (a.Id != null) {
                a.RecordTypeId = personRtId;
                accountsToConvert.add(new Account(Id = a.Id, RecordTypeId = personRtId));
            }
        }
        if (!accountsToConvert.isEmpty()) {
            Database.SaveResult[] acctConvertResults = Database.update(accountsToConvert, false);
            //add error handling as needed
        }
    }

    public void finish(Database.BatchableContext bc) {
        //Add finish handling or logging as needed.
    }
}

When in Doubt, Ask

If your team is struggling to mass-convert to Person Accounts, please reach out. Cloud Notions can help you sidestep many of the obstacles involved.

Want to Learn More?

We'd be happy to dive into details or answer technical questions.  Feel free to contact us today!