The Salesforce Suite module created to connect Drupal to Salesforce is an amazing tool and can help you get up and running with syncing your content right away. It allows you to create individual mappings between Drupal entities and Salesforce objects and have them triggered by CRUD and can be controlled using cron jobs.
Though the Salesforce Suite does not offer the ability to check for existing records along the way and often results in hundreds if not thousands of duplicates. This was a major issue for me since the site I am working with uses Commerce, Commerce Registration, and Entity Registration to allow customers to register and pay for events and courses. We needed this information to be sent to Salesforce and check along the way to see if the registrant(s) were new or if they already existed, and the same goes for their billing information.
But how? The Salesforce Suite only provides the ability of setting a single key field which limits your ability to find a match based on only 1 matching field. This can become a big issue since people share the same names or same email accounts, so you would want to take advantage of looking up records based on a series of criteria. So I was able to utilize the Salesforce API and the provided Salesforce Suite’s hooks to write the checks and take control of the entire import process myself. Note that the following code examples code probably be improved since I am fairly new to building custom modules in Drupal and also fairly new to PHP. Also the following is describing how to connect Commerce and Commerce Registration to Salesforce, so in your case you may need to skip some of this and move on to Part 2 if you are only looking for some simple examples of checking and pushing/updating records.
Creating the Module
In your mymodule_salesforce.module file, we start off with utilizing hook_entity_insert so we can trigger our code after the entity is saved but before we move on.
Starting off with Registrations
function MYMODULE_SALESFORCE_entity_insert($entity, $type) { if (isset($type) && $type == 'registration') { MYMODULE_SALESFORCE_registration_contact_push($entity); global $attendee; $entity->field_salesforce_id['und'][0]['value'] = $attendee; unset($entity->is_new); entity_save('registration', $entity); } //Customer Billing Profile will be loaded here in the next part }
So let’s look at what’s going on in some detail since we are doing a lot in the function above. First we don’t want to run our code for every entity Drupal tries to insert so we check to see if the $type == ‘registration’ before we continue.
Next we call our custom function that will perform the connection with Salesforce and do all the work for us (which we will cover in Part 2). From that, we will pass back the $attendee variable which will contain the Salesforce ID we retrieve from Salesforce. Because I am not using the Salesforce Suite’s configuration, I could not figure a way to set the provided Salesforce Mapping Object ID related field that they use so I do it with my own field, called field_salesforce_id, which is a simple hidden text field on all the entities that I work with through out my custom module (which I recommend you doing the same). Once we get the $attendee back, I set the salesforce_id field’s value to the Salesforce ID that was returned.
In order for the registration entity to properly save the change, we unset is_new before running entity_save.
Customer Billing Information
While we are here, let’s add in one more condition in our mymodule_salesforce_entity_insert function, and that is to look at the Billing Information the user enters in the checkout process. This by default, using the Salesforce Suite, will create random Accounts associated to the Order that aren’t tied to any contact and are very clunky and flood Salesforce with useless records. In order to make this somewhat useful, we want to make sure that if an Account already exists and is tied to a Contact, we want them to be the same and attach them to the Order.
if (isset($type) && $type == 'commerce_customer_profile') { MYMODULE_SALESFORCE_organization_push($entity); global $user; global $organization; $entity->field_salesforce_id['und'][0]['value'] = $organization; $order = commerce_cart_order_id($user->uid); $profiles = commerce_customer_profile_load_multiple(array(), array('uid' => $user->uid)); foreach ($profiles as $profile) { $profile_id = $profile->profile_id; if (isset($profile->entity_context['entity_id']) && $profile->entity_context['entity_id'] == $order){ $customer_profile = commerce_customer_profile_load($profile_id); watchdog('debug', 'Order '. print_r($order, TRUE) .' should have an Org ID of '. print_r($organization, TRUE) .' for Profile ID '. print_r($profile_id, TRUE) .'.'); if ($profile_wrapper = entity_metadata_wrapper('commerce_customer_profile', $customer_profile)) { $profile_wrapper->save(); } } } }
In the above we do some of the same logic as we did with the Registration entity. We check to make sure it’s the commerce_customer_profile before continuing. We then call our custom function that will perform the checks and inserts with Salesforce later and we get back a Salesforce ID which we stored in the variable $organization. Though saving changes into the customer profile in the order isn’t as straight forward as the registration was, especially if you are working with anonymous orders…
So we load the current cart order using the current $user->uid in the session and then load all profiles that match that user ID. This is where it gets a little tricky as you can imagine as anonymous users do not have unique user ID’s since they don’t have accounts so we need to run a loop to check each of the profiles founds and grab their profile ID so we can load the right one later. The best way I found to make sure I have the right user is to check the entity_context[‘entity_id’] against the Order ID to see if they match. Once we find a match, we know we have the right user and can move forward and save the entity with the new Salesforce ID. One thing you will also notice is that I take advantage of adding a watchdog message for debugging later on. I do that a few times throughout this module because the Salesforce Exception messages are not clear or useful enough.
Commerce Order and Commerce Line Items
So this part is a little tricky and some of this may only apply to my organization. One thing, before continuing, that I found is that Commerce Products need to have a Price Book Entry ID set that matches a Price Book Entry up in Salesforce so they can be properly attached to the Order in Salesforce. So before continuing you need to make sure you have first created the Products in Salesforce and their Price Book Entries and then grab those ID’s and assign them to your Products. For me, I used a custom field called field_pricebook_entry_id and I do not push my order or my order products if there is no Price Book Entry ID found in the products attached to my order.
So getting back to it, we utilize the provide hook_commerce_checkout_complete function to perform the following after our order completes.
function MYMODULE_SALESFORCE_commerce_checkout_complete($order){ $order_wrapper = entity_metadata_wrapper('commerce_order', $order); $entity_type = 'commerce_order'; $pricebookEntryID = ''; $pricebookEntryIDValid = TRUE; $pricebookEntryResults = array(); foreach ($order_wrapper->commerce_line_items as $line_item_wrapper) { // load product $product = $line_item_wrapper->commerce_product->value(); $sku = $product->sku; // check for pricebook entry id if(isset($product->field_pricebook_entry_id['und'][0]['value'])){ $pricebookEntryID = $product->field_pricebook_entry_id['und'][0]['value']; // now that we have a pricebookEntryID, let's do a quick check to make sure it's valid $sfapi = salesforce_get_api(); $query = new SalesforceSelectQuery('PricebookEntry'); $query->fields = array('Id'); $query->addCondition('Id', "'" . $pricebookEntryID . "'"); $result = $sfapi->query($query); //If we find the record if ($result['records']) { // Awesome, let's move forward foreach ($result['records'] as $record) { $pricebookEntryIDResults['TRUE'] = $sku; } } } else { // The provided ID was invalid or missing $pricebookEntryIDResults['FALSE'] = $sku; } } // If we stored an invalid result, the order is no longer valid if (array_key_exists('FALSE', $pricebookEntryIDResults)) { $pricebookEntryIDValid = FALSE; $faultProduct = $pricebookEntryIDResults['FALSE']; } // Need to load the customer profile to check for SFID for the Organization $profiles = commerce_customer_profile_load_multiple(array(), array('uid' => $order->uid)); foreach ($profiles as $profile) { $profile_id = $profile->profile_id; if (isset($profile_id) && $profile_id == $order->commerce_customer_billing['und'][0]['profile_id']){ $customer_profile = commerce_customer_profile_load($profile_id); $organization_sfid = $customer_profile->field_salesforce_id['und'][0]['value']; } } // If we have an organization attached to our order with an SFID, then we can move forward with // pushing to Salesforce. Also we want to make sure we have a pricebook entry id to work with // before pushing. if (isset($organization_sfid) && ($pricebookEntryIDValid == TRUE)){ MYMODULE_SALESFORCE_order_push($entity, $organization_sfid); global $orderSFID; $order->order_sfid = $orderSFID; watchdog('debug', 'Order '. print_r($order->order_id, TRUE) .' now has an SFID of '. print_r($order->order_sfid, TRUE) .' saved to its field_order_sfid field.'); } else { if ($organization_sfid == '') { watchdog('warning', 'Order '. print_r($order->order_id, TRUE) .' failed to push to Salesforce because of a missing Organization ID.'); } if ($pricebookEntryIDValid == FALSE) { watchdog('warning', 'Order '. print_r($order->order_id, TRUE) .' failed to push to Salesforce because of an invalid or missing Pricebook Entry ID on Product '. print_r($faultProduct, TRUE) .'.'); } } // Next up, Line Item push to OrderItems if(isset($orderSFID)) { foreach ($order_wrapper->commerce_line_items as $line_item_wrapper) { MYMODULE_SALESFORCE_line_item_push($line_item_wrapper); } } }
That looks like a lot but it’s pretty straight forward and is commented more to help detail what’s going on. But first we load the Order’s Line Items and check the Price Book Entry ID field to make sure it contains a value, after that we do verify the ID to make sure it actually exists in Salesforce and it wasn’t typed in wrong. Now we get a chance to see how the Salesforce connection works so I will break that down in more detail as to what is going on as we will be doing this for our Registration, Customer Billing Information, Order, and Line Items in Part 2.
First we initialize the API call which is a hook provided by the Salesforce Suite.
$sfapi = salesforce_get_api();
Next we setup our query to look at a particular object, in this case the Price Book Entry
$query = new SalesforceSelectQuery('PricebookEntry');
Now we want to specify that we want to look at a particular field during the query that we want to check against so we first list the field in the array (in this case ID) and then we add a condition for the ID field that looks at the value in our Price Book Entry ID field.
$query->fields = array('Id'); $query->addCondition('Id', "'" . $pricebookEntryID . "'");
Now that we have that set, let’s run the query.
$result = $sfapi->query($query);
If we find records in our query, we know we have a valid Price Book Entry ID and we can be happy and move forward with our code. Previously in my code I setup a new empty array to store if the Price Book Entry was valid and also what the SKU was for the Product it checked on. This is useful for a later watchdog message so I can see what Product failed so I can find it quickly and update it’s ID so future orders don’t fail to push.
if ($result['records']) { foreach ($result['records'] as $record) { $pricebookEntryIDResults['TRUE'] = $sku; } }
After we checked for a valid ID we move forward with checking to make sure we also have a valid Organization SFID which was the Salesforce ID returned and saved in our Customer Billing Profile. If everything checks out, we move forward with calling our custom mymodule_salesforce_order_push function to push the Order up and log a success message if we get a valid Salesforce ID returned to us after we are done.
If we do not have a valid ID, the Order will fail to push because it requires a previously created Account to be attached to the Order. If either the Organization SFID or the Price Book Entry ID return invalid, we set some watchdog messages to record the problem so we can look into them later.
Lastly after the Order is sent up, we need to loop through each of the attached Line Items and call our custom mymodule_salesforce_line_item_push function.
If you enjoyed what you read here, please leave a comment and read on to find out more at Part 2.