/salesforce_ar_sync

ActiveRecord extension & rails engine for syncing data with Salesforce.com

Primary LanguageRubyMIT LicenseMIT

SalesforceArSync

SalesforceARSync allows you to sync models and fields with Salesforce through a combination of Outbound Messaging, SOAP and restforce.

Installation

Requirements

Salesforce Setup

Before you can start syncing your data several things must be completed in Salesforce.

1. Setup Remote Access

Create a new Remote Access Application entry by going to

Setup -> Develop -> Remote Access

You can use http://localhost/nothing for the Callback URL

2. Setup Outbound Messaging

Each model you wish to sync requires a workflow to trigger outbound messaging. You can set the worflow to trigger on the specific fields you wish to update.

Setup -> Create -> Workflow & Approvals -> Worflow Rules

Click New Rule, select the object (model) you wish to sync and click Next, give the rule a name, select Every time a record is created or edited and set a rule on the field(s) you want to sync ( a formula checking if the fields have changed is recommended). Click Save & Next, in the Add Worflow Action dropdown select New Outbound Message. Enter a name and set the Endpoint URL to be http://yoursite.com/integration/sf_soap/model_name. Select the fields you wish to sync (Id and SystemModstamp are required).

*You need to do this for each object/model you want to sync.

restforce

Before using the salesforce_ar_sync gem you must ensure you have the restforce gem installed and configured properly. Make sure each of the models you wish to sync are materialized.

# Note: You can name the config to anything you want
$sf_client = Restforce::Client.new("config/salesforce.yml")
$sf_client.authenticate username: <username>, password: <password>

module SalesforceArSync::SalesforceSync
  SF_CLIENT = $sf_client
end

Gem Installation

Add this line to your application's Gemfile:

gem 'salesforce_ar_sync'

And then execute:

$ bundle

Or install it yourself as:

$ gem install salesforce_ar_sync

Application Setup

Before using the gem you must complete the setup of your rails app.

The gem needs to know your 18 character organization id, it can be stored in a YAML file or in the ENV class.

To create the yaml file run

$ rails generate salesforce_ar_sync:configuration <organization id>

Next you will need to decide which models you want to sync. For each model you must create a migration and run them

$ rails generate salesforce_ar_sync:migrations <models> --migrate

To mount the engine add the following line to your routes.rb file

mount SalesforceArSync::Engine => '/integration'

You can change '/integration' to whatever you want, all of the engine routes will be based off of this path. Running

$ rake routes | grep salesforce_ar_sync

will show you all of the gems routes, make sure you point your outbound messages at these urls.

Next you will need to tell the gem which models are syncable by adding salesforce_syncable to your model class and specifying which attributes you would like to sync.

salesforce_syncable :sync_attributes => {FirstName: :first_name, LastName: :last_name}

The first parameter in the :sync_attributes hash is the Salesforce field name and the second is the model attribute name.

Usage

Configuration Options

The gem can be configured using a YAML file or with the ENV variable.

The options available to configure are

  • organization_id: the 18 character organization id of your Salesforce instance
  • sync_enabled: a global sync enabled flag which is a boolean true/false
  • namespace_prefix: Namespace prefix of your Salesforce app in case you specified one
  • deletion_map: Salesforce object names mapped to internal app name.
  • job_queue: A symbol naming the ActiveJob queue you wish sync and delete jobs to use. (default is :default)

To generate a YAML file

$ rails generate salesforce_ar_sync:configuration

Or with an organization id

$ rails generate salesforce_ar_sync:configuration 123456789123456789

which will create a template salesforce_ar_sync.yml in /config that looks like the following

organization_id: <organization id> #18 character organization_id
sync_enabled: true
namespace_prefix:
deletion_map:

To use the ENV variable you must pass environment variables to rails via the export command in bash or before the initializer loads the ENV settings.

$ export SALESFORCE_AR_SYNC_ORGANIZATION_ID=123456789123456789
$ export SALESFORCE_AR_SYNC_SYNC_ENABLED=true
$ export SALESFORCE_AR_NAMESPACE_PREFIX=my_prefix

An example of adding an aliased object to the deletion map should look like the following:

deletion_map:
 -
  Account: 'YourModelName'

Model Options

The model can have several options set:

salesforce_sync_enabled

sync_attributes

async_attributes

default_attributes_for_create

salesforce_id_attribute_name

web_id_attribute_name

activerecord_web_id_attribute_name

salesforce_sync_web_id

web_class_name

salesforce_object_name

except

save_method

unscoped_updates

readonly_fields

salesforce_sync_enabled

Model level option to enable disable the sync, defaults to true.

salesforce_sync_enabled: false

sync_attributes

Hash mapping of Salesforce attributes to web attributes, defaults to empty hash. "Web" attributes can be actual method names to return a custom value.If you are providing a method name to return a value, you should also implement a corresponding my_method_changed? to return if the value has changed. Otherwise it will always be synced.

sync_attributes: { Email: :login, FirstName: :first_name, LastName: :last_name }

async_attributes

An array of Salesforce attributes which should be synced asynchronously, defaults to an empty array. When an object is saved and only attributes contained in this array, the save to Salesforce will be queued and processed asyncronously. Use this carefully, nothing is done to ensure data integrity, if multiple jobs are queued for a single object there is no way to guarentee that they are processed in order, or that the save to Salesforce will succeed.

async_attributes: ['Last_Login__c', 'Login_Count__c']

Note: The model will fall back to synchronous sync if non-synchronous attributes are changed along with async attributes

default_attributes_for_create

A hash of default attributes that should be used when we are creating a new record, defaults to empty hash.

default_attributes_for_create: { password_change_required: true }

salesforce_id_attribute_name

The "Id" attribute of the corresponding Salesforce object, defaults to Id.

salesforce_id_attribute_name: :Id

web_id_attribute_name

The field name of the web id attribute in the Salesforce Object, defaults to WebId__c

web_id_attribute_name: :WebId__c

activerecord_web_id_attribute_name

The field name of the web id attribute in the Active Record Object, defaults to id

activerecord_web_id_attribute_name: :id

salesforce_sync_web_id

Enable or disable sync of the web id, defaults to false. Use this if you have a need for the id field of the ActiveRecord model to by synced to Salesforce.

salesforce_sync_web_id: false

additional_lookup_fields

Optionally can specify what fields can be used for finding the object that should be updated if object was not found by salesforce id or web id

additional_lookup_fields: { login: :User_ID_Email__c }

web_class_name

The name of the Web Objects class. A custom value can be provided if you wish to sync to a SF object and back to a different web object. Defaults to the model name. This would generally be used if you wanted to flatten a web object into a larger SF object like Contact.

web_class_name: 'Contact',

salesforce_object_name

Optionally holds the name of a method which will return the name of the Salesforce object to sync to, defaults to nil.

salesforce_object_name: :salesforce_object_name_method_name

except

Optionally holds the name of a method which can contain logic to determine if a record should be synced on save. If no method is given then only the salesforce_skip_sync attribute is used. Defaults to nil.

except: :except_method_name

save_method

Optionally holds the name of a method which contains custom logic for saving on sync. Defaults to ActiveRecord::Base#save!

save_method: :save_method_name

unscoped_updates

Enable bypassing the default scope when searching for records to update. This is useful when using a soft deletion strategy that can respect SF undeletion.

unscoped_updates: true

readonly_fields

Optionally set fields on the salesforce object that have been defined as Read Only in Salesforce. This helps to ensure that those fields are not synced from the model to salesforce (but still syncable the other way). Accepts the salesforce field, not the model's fields.

readonly_fields: %i[NumberOfPosts__c ActiveSeats__c]

Stopping the Sync

Stopping the gem from syncing can be done on three levels.

  • The global level before the app starts via the .yml file, ENV variables or after the app starts with the gem's configuration variable SALESFORCE_AR_SYNC_CONFIG["SYNC_ENABLED"]
  • The model level by setting the :salesforce_sync_enabled => false or :except => :method_name
  • The instance level by setting :salesforce_skip_sync => true in the instance

Examples

Our Basic Example Model

class Contact < ActiveRecord::Base
  attributes :first_name, :last_name, :phone, :email, :last_login_time, :salesforce_id, :salesforce_updated_at
  attr_accessor :first_name, :last_name, :phone, :email, :last_login_time
end

Making the Model Syncable

class Contact < ActiveRecord::Base
  attributes :first_name, :last_name, :phone, :email, :last_login_time, :salesforce_id, :salesforce_updated_at
  attr_accessor :first_name, :last_name, :phone, :email

  salesforce_syncable sync_attributes: { FirstName: :first_name, LastName: :last_name, Phone: :phone, Email: :email }
end

Stopping the Model from Syncing with a Flag

class Contact < ActiveRecord::Base
  attributes :first_name, :last_name, :phone, :email, :last_login_time, :salesforce_id, :salesforce_updated_at
  attr_accessor :first_name, :last_name, :phone, :email

  salesforce_syncable sync_attributes: {FirstName: :first_name, LastName: :last_name, Phone: :phone, Email: :email},
                      salesforce_sync_enabled: false
end

Stopping the Model from Syncing with a Method

class Contact < ActiveRecord::Base
  attributes :first_name, :last_name, :phone, :email, :last_login_time, :salesforce_id, :salesforce_updated_at
  attr_accessor :first_name, :last_name, :phone, :email

  salesforce_syncable sync_attributes: {FirstName: :first_name, LastName: :last_name, Phone: :phone, Email: :email},
                      except: :skip_sync?

  def skip_sync?
    if first_name.blank?
      return true
    end
  end
end

Stopping a Record from Syncing

customer = Contact.find_by_email('test@example.com')
customer.salesforce_skip_sync = true

Specify Async Attributes

class Contact < ActiveRecord::Base
  attributes :first_name, :last_name, :phone, :email, :last_login_time, :salesforce_id, :salesforce_updated_at
  attr_accessor :first_name, :last_name, :phone, :email, :last_login_time

  salesforce_syncable sync_attributes: { FirstName: :first_name, LastName: :last_name, Phone: :phone, Email: :email, Last_Login_Time__c: :last_login_time },
                      async_attributes: ['Last_Login_Time__c']
end

Specify Default Attributes when an Object is Created

class Contact < ActiveRecord::Base
  attributes :first_name, :last_name, :phone, :email, :last_login_time, :salesforce_id, :salesforce_updated_at
  attr_accessor :first_name, :last_name, :phone, :email, :last_login_time

  salesforce_syncable sync_attributes: { FirstName: :first_name, LastName: :last_name, Phone: :phone, Email: :email },
                      default_attributes_for_create: { password_change_required: true }
end

Relationships

If you want to keep the standard ActiveRecord associations in place, but need to populate these relationships from Salesforce records, you can define methods in your models to add to the attribute mapping.

The following example shows a Contact model, which is related to an Account model through account_id, we implement a getter, setter and _changed? method to do our lookups and map these methods in our sync_attributes mapping instead of the standard attributes. This allows us to send/receive messages from Salesforce using the 18 digit Salesforce id, but maintain our ActiveRecord relationships.

	class Contact < ActiveRecord::Base
		attributes :first_name, :last_name, :account_id
		attr_accessor :first_name, :last_name, :account_id

		salesforce_syncable sync_attributes: { FirstName: :first_name,
																							LastName: :last_name,
																							AccountId: :salesforce_account_id }

		def salesforce_account_id_changed?
		  account_id_changed?
		end

		def salesforce_account_id
		  return nil if account_id.nil?
		  account.salesforce_id
		end

		def salesforce_account_id=(account_id)
		  self.account = nil if account_id.nil? and return
		  self.account = Account.find_or_create_by_salesforce_id(account_id)
		end
	end

Defining a Custom Salesforce Object

class Contact < ActiveRecord::Base
  attributes :first_name, :last_name, :phone, :email, :last_login_time, :salesforce_id, :salesforce_updated_at
  attr_accessor :first_name, :last_name, :phone, :email, :last_login_time

  salesforce_syncable sync_attributes: { FirstName: :first_name, LastName: :last_name, Phone: :phone, Email: :email },
                      salesforce_object_name: :custom_salesforce_object_name

  def custom_salesforce_object_name
    "CustomContact__c"
  end
end

Deletes

Inbound Deletes

In order to handle the delete of objects coming from Salesforce, a bit of code is necessary because an Outbound Message cannot be triggered when an object is deleted. To work around this you will need to create a new Custom Object in your Salesforce environment:

	Deleted_Object__C
		Object_Id__c_ => Text(18)
		Object_Type__c_ => Text(255)

Object_Id__c will hold the 18 digit Id of the record being deleted. Object_Type__c will hold the name of the Rails Model that the Salesforce object is synced with.

If you trigger a record to be written to this object whenever another object is deleted, and configure an Outbound Message to send to the /sf_soap/delete action whenever a Deleted_Object__c record is created, the corresponding record will be removed from your Rails app.

Syncing inbound deletes is enabled by default, but can be configured in the Rails Model. This is done using the :sync_inbound_delete option, which can take either a boolean value, or the name of a method that returns a boolean value.

  salesforce_syncable sync_inbound_delete: :inbound_delete
                     #sync_inbound_delete: true
  def inbound_delete
    return self.comments.count == 0
  end

Outbound Deletes

Syncing outbound deletes to Salesforce is disabled by default, but can be configured in the Rails Model. This is done using the :sync_outbound_delete option, which can take either a boolean value, or the name of a method that returns a boolean value.

  salesforce_syncable sync_outbound_delete: :outbound_delete
                     #sync_outbound_delete: false

  def outbound_delete
    return self.is_trial_user?
  end

Soft Deletes

Setting unscoped_updates to true will permit you to find deleted objects to sync changes to. You can implement virtual attributes matching the undelete field in SF to implement the soft deletion strategy of your choosing. For the paranoia gem:

  def undeleted=(value)
    restore if value
  end

Errors

Outbound Message Errors

If the SOAP handler encounters an error it will be recorded in the log of the outbound message in Salesforce. To view the message go to

Setup -> Monitoring -> Outbound Messages

Finding your 18 Character Organization ID

Your 15 character organization id can be found in Setup -> Company Profile -> Company Information. You must convert it to an 18 character id by running it through the tool located here: http://cloudjedi.wordpress.com/no-fuss-salesforce-id-converter/ or by installing the Force.com Utility Belt for Chrome.

Contributing

  1. Fork it
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Added some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create new Pull Request