salesforce_ar_sync 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +22 -0
- data/README.md +413 -0
- data/lib/generators/salesforce_ar_sync/configuration/configuration_generator.rb +17 -0
- data/lib/generators/salesforce_ar_sync/configuration/templates/salesforce_ar_sync.yml +17 -0
- data/lib/generators/salesforce_ar_sync/migrations/migrations_generator.rb +53 -0
- data/lib/generators/salesforce_ar_sync/migrations/templates/migration.rb +13 -0
- data/lib/salesforce_ar_sync.rb +19 -0
- data/lib/salesforce_ar_sync/engine.rb +9 -0
- data/lib/salesforce_ar_sync/extenders/salesforce_syncable.rb +57 -0
- data/lib/salesforce_ar_sync/ip_constraint.rb +17 -0
- data/lib/salesforce_ar_sync/salesforce_object_sync.rb +16 -0
- data/lib/salesforce_ar_sync/salesforce_sync.rb +197 -0
- data/lib/salesforce_ar_sync/soap_handler/base.rb +57 -0
- data/lib/salesforce_ar_sync/soap_handler/delete.rb +19 -0
- data/lib/salesforce_ar_sync/version.rb +3 -0
- metadata +212 -0
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 Liam Nediger
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,413 @@
|
|
1
|
+
# SalesforceArSync
|
2
|
+
|
3
|
+
SalesforceARSync allows you to sync models and fields with Salesforce through a combination of
|
4
|
+
Outbound Messaging, SOAP and databasedotcom.
|
5
|
+
|
6
|
+
## Installation
|
7
|
+
|
8
|
+
### Requirements
|
9
|
+
|
10
|
+
* Rails >= 3.1
|
11
|
+
* Salesforce.com instance
|
12
|
+
* [Have your 18 character organization id ready](#finding-your-18-character-organization-id)
|
13
|
+
* databasedotcom gem >= 1.3 installed and configured [see below](#databasedotcom)
|
14
|
+
* delayed_job gem >= 3.0 installed and configured
|
15
|
+
|
16
|
+
### Salesforce Setup
|
17
|
+
|
18
|
+
Before you can start syncing your data several things must be completed in Salesforce.
|
19
|
+
|
20
|
+
#### 1. Setup Remote Access
|
21
|
+
Create a new Remote Access Application entry by going to
|
22
|
+
|
23
|
+
Setup -> Develop -> Remote Access
|
24
|
+
|
25
|
+
You can use http://localhost/nothing for the _Callback URL_
|
26
|
+
|
27
|
+
#### 2. Setup Outbound Messaging
|
28
|
+
Each model you wish to sync requires a workflow to trigger outbound messaging. You can set the worflow
|
29
|
+
to trigger on the specific fields you wish to update.
|
30
|
+
|
31
|
+
Setup -> Create -> Workflow & Approvals -> Worflow Rules
|
32
|
+
|
33
|
+
Click _New Rule_, select the object (model) you wish to sync and click _Next_, give the rule a name, select
|
34
|
+
_Every time a record is created or edited_ and set a rule on the field(s) you want to sync ( a formula checking
|
35
|
+
if the fields have changed is recommended). Click _Save & Next_, in the _Add Worflow Action_ dropdown select
|
36
|
+
_New Outbound Message_. Enter a name and set the _Endpoint URL_ to be http://yoursite.com/integration/sf_soap/model_name.
|
37
|
+
Select the fields you wish to sync (Id and SystemModstamp are required).
|
38
|
+
|
39
|
+
*You need to do this for each object/model you want to sync.
|
40
|
+
|
41
|
+
### databasedotcom
|
42
|
+
|
43
|
+
Before using the salesforce_ar_sync gem you must ensure you have the databasedotcom gem installed and configured
|
44
|
+
properly. Make sure each of the models you wish to sync are materialized.
|
45
|
+
|
46
|
+
````ruby
|
47
|
+
$sf_client = Databasedotcom::Client.new("config/databasedotcom.yml")
|
48
|
+
$sf_client.authenticate :username => <username>, :password => <password>
|
49
|
+
$sf_client.sobject_module = Databasedotcom
|
50
|
+
$sf_client.materialize "User"
|
51
|
+
````
|
52
|
+
|
53
|
+
### Gem Installation
|
54
|
+
|
55
|
+
Add this line to your application's Gemfile:
|
56
|
+
|
57
|
+
gem 'salesforce_ar_sync'
|
58
|
+
|
59
|
+
And then execute:
|
60
|
+
|
61
|
+
$ bundle
|
62
|
+
|
63
|
+
Or install it yourself as:
|
64
|
+
|
65
|
+
$ gem install salesforce_ar_sync
|
66
|
+
|
67
|
+
### Application Setup
|
68
|
+
|
69
|
+
Before using the gem you must complete the setup of your rails app.
|
70
|
+
|
71
|
+
The gem needs to know your 18 character organization id, it can be stored in a YAML file or in the ENV class.
|
72
|
+
|
73
|
+
To create the yaml file run
|
74
|
+
|
75
|
+
$ rails generate salesforce_ar_sync:configuration <organization id>
|
76
|
+
|
77
|
+
Next you will need to decide which models you want to sync. For each model you must create a migration and run them
|
78
|
+
|
79
|
+
$ rails generate salesforce_ar_sync:migrations <models> --migrate
|
80
|
+
|
81
|
+
To mount the engine add the following line to your routes.rb file
|
82
|
+
|
83
|
+
mount SalesforceArSync::Engine => '/integration'
|
84
|
+
|
85
|
+
You can change '/integration' to whatever you want, all of the engine routes will be based off of this path. Running
|
86
|
+
|
87
|
+
$ rake routes | grep salesforce_ar_sync
|
88
|
+
|
89
|
+
will show you all of the gems routes, make sure you point your outbound messages at these urls.
|
90
|
+
|
91
|
+
Next you will need to tell the gem which models are syncable by adding _salesforce_syncable_ to your model class
|
92
|
+
and specifying which attributes you would like to sync.
|
93
|
+
|
94
|
+
````ruby
|
95
|
+
salesforce_syncable :sync_attributes => {:FirstName => :first_name, :LastName => :last_name}
|
96
|
+
````
|
97
|
+
|
98
|
+
The first parameter in the _:sync_attributes_ hash is the Salesforce field name and the second is the model attribute
|
99
|
+
name.
|
100
|
+
|
101
|
+
## Usage
|
102
|
+
|
103
|
+
### Configuration Options
|
104
|
+
|
105
|
+
The gem can be configured using a YAML file or with the ENV variable.
|
106
|
+
|
107
|
+
The options available to configure are
|
108
|
+
|
109
|
+
* __organization_id__: the 18 character organization id of your Salesforce instance
|
110
|
+
* __sync_enabled__: a global sync enabled flag which is a boolean true/false
|
111
|
+
|
112
|
+
To generate a YAML file
|
113
|
+
|
114
|
+
$ rails generate salesforce_ar_sync:configuration
|
115
|
+
|
116
|
+
Or with an organization id
|
117
|
+
|
118
|
+
$ rails generate salesforce_ar_sync:configuration 123456789123456789
|
119
|
+
|
120
|
+
which will create a template salesforce_ar_sync.yml in /config that looks like the following
|
121
|
+
|
122
|
+
organization_id: <organization id> #18 character organization_id
|
123
|
+
sync_enabled: true
|
124
|
+
|
125
|
+
|
126
|
+
To use the ENV variable you must pass environemnt variables to rails via the _export_ command in bash or before the
|
127
|
+
initializer loads the ENV settings.
|
128
|
+
|
129
|
+
$ export SALESFORCE_AR_SYNC_ORGANIZATION_ID=123456789123456789
|
130
|
+
$ export SALESFORCE_AR_SYNC_SYNC_ENABLED=true
|
131
|
+
|
132
|
+
### Model Options
|
133
|
+
The model can have several options set:
|
134
|
+
|
135
|
+
[__salesforce_sync_enabled__](#salesforce_sync_enabled)
|
136
|
+
[__sync_attributes__](#sync_attributes)
|
137
|
+
[__async_attributes__](#async_attributes)
|
138
|
+
[__default_attributes_for_create__](#default_attributes_for_create)
|
139
|
+
[__salesforce_id_attribute_name__](#salesforce_id_attribute_name)
|
140
|
+
[__web_id_attribute_name__](#web_id_attribute_name)
|
141
|
+
[__salesforce_sync_web_id__](#salesforce_sync_web_id)
|
142
|
+
[__web_class_name__](#web_class_name)
|
143
|
+
[__salesforce_object_name__](#salesforce_object_name)
|
144
|
+
[__salesforce_object__](#salesforce_object)
|
145
|
+
[__except__](#except)
|
146
|
+
|
147
|
+
#### <a id="salesforce_sync_enabled"></a>salesforce_sync_enabled
|
148
|
+
Model level option to enable disable the sync, defaults to true.
|
149
|
+
|
150
|
+
````ruby
|
151
|
+
:salesforce_sync_enabled => false
|
152
|
+
````
|
153
|
+
|
154
|
+
#### sync_attributes
|
155
|
+
Hash mapping of Salesforce attributes to web attributes, defaults to empty hash.
|
156
|
+
"Web" attributes can be actual method names to return a custom value.If you are providing a method name to return a
|
157
|
+
value, you should also implement a corresponding my_method_changed? to return if the value has changed. Otherwise
|
158
|
+
it will always be synced.
|
159
|
+
|
160
|
+
````ruby
|
161
|
+
:sync_attributes => { :Email => :login, :FirstName => :first_name, :LastName => :last_name }
|
162
|
+
````
|
163
|
+
|
164
|
+
#### async_attributes
|
165
|
+
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.
|
166
|
+
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.
|
167
|
+
|
168
|
+
````ruby
|
169
|
+
:async_attributes => ["Last_Login__c", "Login_Count__c"]
|
170
|
+
````
|
171
|
+
|
172
|
+
Note: The model will fall back to synchronous sync if non-synchronous attributes are changed along with async
|
173
|
+
attributes
|
174
|
+
|
175
|
+
#### default_attributes_for_create
|
176
|
+
A hash of default attributes that should be used when we are creating a new record, defaults to empty hash.
|
177
|
+
|
178
|
+
````ruby
|
179
|
+
:default_attributes_for_create => {:password_change_required => true}
|
180
|
+
````
|
181
|
+
|
182
|
+
#### salesforce_id_attribute_name
|
183
|
+
The "Id" attribute of the corresponding Salesforce object, defaults to _Id_.
|
184
|
+
|
185
|
+
````ruby
|
186
|
+
:salesforce_id_attribute_name => :Id
|
187
|
+
````
|
188
|
+
|
189
|
+
#### web_id_attribute_name
|
190
|
+
The field name of the web id attribute in the Salesforce Object, defaults to _WebId__c_
|
191
|
+
|
192
|
+
````ruby
|
193
|
+
:web_id_attribute_name => :WebId__c
|
194
|
+
````
|
195
|
+
|
196
|
+
#### salesforce_sync_web_id
|
197
|
+
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.
|
198
|
+
|
199
|
+
````ruby
|
200
|
+
:salesforce_sync_web_id => false
|
201
|
+
````
|
202
|
+
|
203
|
+
#### web_class_name
|
204
|
+
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
|
205
|
+
different web object. Defaults to the model name. This would generally be used if you wanted to flatten a web object
|
206
|
+
into a larger SF object like Contact.
|
207
|
+
|
208
|
+
````ruby
|
209
|
+
:web_class_name => 'Contact',
|
210
|
+
````
|
211
|
+
|
212
|
+
#### salesforce_object_name
|
213
|
+
Optionally holds the name of a method which will return the name of the Salesforce object to sync to, defaults to nil.
|
214
|
+
|
215
|
+
````ruby
|
216
|
+
:salesforce_object_name => :salesforce_object_name_method_name
|
217
|
+
````
|
218
|
+
|
219
|
+
#### salesforce_object
|
220
|
+
Optionally holds the name of a method which will retrieve a Salesforce object. Default implementation is called if no
|
221
|
+
method is specified, defaults to nil.
|
222
|
+
|
223
|
+
````ruby
|
224
|
+
:salesforce_object => :salesforce_object_method_name
|
225
|
+
````
|
226
|
+
|
227
|
+
#### except
|
228
|
+
Optionally holds the name of a method which can contain logic to determine if a record should be synced on save. If no
|
229
|
+
method is given then only the salesforce_skip_sync attribute is used. Defaults to nil.
|
230
|
+
|
231
|
+
````ruby
|
232
|
+
:except => :except_method_name
|
233
|
+
````
|
234
|
+
|
235
|
+
### Stopping the Sync
|
236
|
+
|
237
|
+
Stopping the gem from syncing can be done on three levels.
|
238
|
+
|
239
|
+
* The global level before the app starts via the .yml file, ENV variables or after the app starts with the gem's
|
240
|
+
configuration variable _SALESFORCE_AR_SYNC_CONFIG["SYNC_ENABLED"]_
|
241
|
+
* The model level by setting the _:salesforce_sync_enabled => false_ or _:except => :method_name_
|
242
|
+
* The instance level by setting _:salesforce_skip_sync => true_ in the instance
|
243
|
+
|
244
|
+
## Examples
|
245
|
+
|
246
|
+
### Our Basic Example Model
|
247
|
+
|
248
|
+
```ruby
|
249
|
+
class Contact < ActiveRecord::Base
|
250
|
+
attributes :first_name, :last_name, :phone, :email, :last_login_time, :salesforce_id, :salesforce_updated_at
|
251
|
+
attr_accessor :first_name, :last_name, :phone, :email, :last_login_time
|
252
|
+
end
|
253
|
+
```
|
254
|
+
|
255
|
+
### Making the Model Syncable
|
256
|
+
|
257
|
+
```ruby
|
258
|
+
class Contact < ActiveRecord::Base
|
259
|
+
attributes :first_name, :last_name, :phone, :email, :last_login_time, :salesforce_id, :salesforce_updated_at
|
260
|
+
attr_accessor :first_name, :last_name, :phone, :email
|
261
|
+
|
262
|
+
salesforce_syncable :sync_attributes => {:FirstName => :first_name, :LastName => :last_name, :Phone => :phone, :Email => :email}
|
263
|
+
end
|
264
|
+
```
|
265
|
+
|
266
|
+
### Stopping the Model from Syncing with a Flag
|
267
|
+
|
268
|
+
```ruby
|
269
|
+
class Contact < ActiveRecord::Base
|
270
|
+
attributes :first_name, :last_name, :phone, :email, :last_login_time, :salesforce_id, :salesforce_updated_at
|
271
|
+
attr_accessor :first_name, :last_name, :phone, :email
|
272
|
+
|
273
|
+
salesforce_syncable :sync_attributes => {:FirstName => :first_name, :LastName => :last_name, :Phone => :phone, :Email => :email},
|
274
|
+
:salesforce_sync_enabled => false
|
275
|
+
end
|
276
|
+
```
|
277
|
+
|
278
|
+
### Stopping the Model from Syncing with a Method
|
279
|
+
|
280
|
+
```ruby
|
281
|
+
class Contact < ActiveRecord::Base
|
282
|
+
attributes :first_name, :last_name, :phone, :email, :last_login_time, :salesforce_id, :salesforce_updated_at
|
283
|
+
attr_accessor :first_name, :last_name, :phone, :email
|
284
|
+
|
285
|
+
salesforce_syncable :sync_attributes => {:FirstName => :first_name, :LastName => :last_name, :Phone => :phone, :Email => :email},
|
286
|
+
:except => :skip_sync?
|
287
|
+
|
288
|
+
def skip_sync?
|
289
|
+
if first_name.blank?
|
290
|
+
return true
|
291
|
+
end
|
292
|
+
end
|
293
|
+
end
|
294
|
+
```
|
295
|
+
|
296
|
+
### Stopping a Record from Syncing
|
297
|
+
|
298
|
+
```ruby
|
299
|
+
customer = Contact.find_by_email('test@example.com')
|
300
|
+
customer.salesforce_skip_sync = true
|
301
|
+
```
|
302
|
+
|
303
|
+
### Specify Async Attributes
|
304
|
+
|
305
|
+
```ruby
|
306
|
+
class Contact < ActiveRecord::Base
|
307
|
+
attributes :first_name, :last_name, :phone, :email, :last_login_time, :salesforce_id, :salesforce_updated_at
|
308
|
+
attr_accessor :first_name, :last_name, :phone, :email, :last_login_time
|
309
|
+
|
310
|
+
salesforce_syncable :sync_attributes => {:FirstName => :first_name, :LastName => :last_name, :Phone => :phone, :Email => :email, :Last_Login_Time__c => :last_login_time},
|
311
|
+
:async_attributes => ["Last_Login_Time__c"]
|
312
|
+
end
|
313
|
+
```
|
314
|
+
|
315
|
+
### Specify Default Attributes when an Object is Created
|
316
|
+
|
317
|
+
```ruby
|
318
|
+
class Contact < ActiveRecord::Base
|
319
|
+
attributes :first_name, :last_name, :phone, :email, :last_login_time, :salesforce_id, :salesforce_updated_at
|
320
|
+
attr_accessor :first_name, :last_name, :phone, :email, :last_login_time
|
321
|
+
|
322
|
+
salesforce_syncable :sync_attributes => {:FirstName => :first_name, :LastName => :last_name, :Phone => :phone, :Email => :email},
|
323
|
+
:default_attributes_for_create => {:password_change_required => true}
|
324
|
+
end
|
325
|
+
```
|
326
|
+
|
327
|
+
### Relationships
|
328
|
+
If you want to keep the standard ActiveRecord associations in place, but need to populate these relationships from Salesforce records, you can define
|
329
|
+
methods in your models to add to the attribute mapping.
|
330
|
+
|
331
|
+
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
|
332
|
+
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
|
333
|
+
using the 18 digit Salesforce id, but maintain our ActiveRecord relationships.
|
334
|
+
|
335
|
+
```ruby
|
336
|
+
|
337
|
+
class Contact < ActiveRecord::Base
|
338
|
+
attributes :first_name, :last_name, :account_id
|
339
|
+
attr_accessor :first_name, :last_name, :account_id
|
340
|
+
|
341
|
+
salesforce_syncable :sync_attributes => { :FirstName => :first_name,
|
342
|
+
:LastName => :last_name,
|
343
|
+
:AccountId => :salesforce_account_id }
|
344
|
+
|
345
|
+
def salesforce_account_id_changed?
|
346
|
+
account_id_changed?
|
347
|
+
end
|
348
|
+
|
349
|
+
def salesforce_account_id
|
350
|
+
return nil if account_id.nil?
|
351
|
+
account.salesforce_id
|
352
|
+
end
|
353
|
+
|
354
|
+
def salesforce_account_id=(account_id)
|
355
|
+
self.account = nil and return if account_id.nil?
|
356
|
+
self.account = Account.find_or_create_by_salesforce_id(account_id)
|
357
|
+
end
|
358
|
+
end
|
359
|
+
|
360
|
+
```
|
361
|
+
|
362
|
+
### Defining a Custom Salesforce Object
|
363
|
+
|
364
|
+
```ruby
|
365
|
+
class Contact < ActiveRecord::Base
|
366
|
+
attributes :first_name, :last_name, :phone, :email, :last_login_time, :salesforce_id, :salesforce_updated_at
|
367
|
+
attr_accessor :first_name, :last_name, :phone, :email, :last_login_time
|
368
|
+
|
369
|
+
salesforce_syncable :sync_attributes => {:FirstName => :first_name, :LastName => :last_name, :Phone => :phone, :Email => :email},
|
370
|
+
:salesforce_object => :custom_salesforce_object
|
371
|
+
|
372
|
+
def custom_salesforce_object
|
373
|
+
"CustomContact__c"
|
374
|
+
end
|
375
|
+
end
|
376
|
+
```
|
377
|
+
|
378
|
+
## Deletes
|
379
|
+
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
|
380
|
+
an object is deleted. To work around this you will need to create a new Custom Object in your Salesforce environment:
|
381
|
+
|
382
|
+
```
|
383
|
+
Deleted_Object__C
|
384
|
+
Object_Id__c_ => Text(18)
|
385
|
+
Object_Type__c_ => Text(255)
|
386
|
+
```
|
387
|
+
|
388
|
+
Object_Id__c will hold the 18 digit Id of the record being deleted.
|
389
|
+
Object_Type__c will hold the name of the Rails Model that the Salesforce object is synced with.
|
390
|
+
|
391
|
+
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
|
392
|
+
whenever a Deleted_Object__c record is created, the corresponding record will be removed from your Rails app.
|
393
|
+
|
394
|
+
## Errors
|
395
|
+
|
396
|
+
### Outbound Message Errors
|
397
|
+
|
398
|
+
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
|
399
|
+
|
400
|
+
Setup -> Monitoring -> Outbound Messages
|
401
|
+
|
402
|
+
## <a id="orga_id"></a>Finding your 18 Character Organization ID
|
403
|
+
Your 15 character organization id can be found in _Setup -> Company Profile -> Company Information_. You must convert
|
404
|
+
it to an 18 character id by running it through the tool located here:
|
405
|
+
http://cloudjedi.wordpress.com/no-fuss-salesforce-id-converter/ or by installing the Force.com Utility Belt for Chrome.
|
406
|
+
|
407
|
+
## Contributing
|
408
|
+
|
409
|
+
1. Fork it
|
410
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
411
|
+
3. Commit your changes (`git commit -am 'Added some feature'`)
|
412
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
413
|
+
5. Create new Pull Request
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module SalesforceArSync
|
2
|
+
module Generators
|
3
|
+
class ConfigurationGenerator < Rails::Generators::Base
|
4
|
+
include Rails::Generators::Migration
|
5
|
+
|
6
|
+
source_root File.expand_path('../templates', __FILE__)
|
7
|
+
|
8
|
+
desc "Generates migrations to add required columns for Salesforce sync on specified models."
|
9
|
+
|
10
|
+
argument :organization_id, :type => :string, :banner => "organization_id", :default => "#18 character organization_id"
|
11
|
+
|
12
|
+
def create_yaml
|
13
|
+
template "salesforce_ar_sync.yml", "config/salesforce_ar_sync.yml"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
production:
|
2
|
+
organization_id: <%= @organization_id %>
|
3
|
+
sync_enabled: true
|
4
|
+
# Salesforce owned IPs from: https://help.salesforce.com/apex/HTViewSolution?language=en_US&id=000003652
|
5
|
+
ip_ranges: "204.14.232.0/23,204.14.237.0/24,96.43.144.0/22,96.43.148.0/22,204.14.234.0/23,204.14.238.0/23,182.50.76.0/22"
|
6
|
+
|
7
|
+
development:
|
8
|
+
organization_id: <%= @organization_id %>
|
9
|
+
sync_enabled: false
|
10
|
+
# Salesforce owned IPs from: https://help.salesforce.com/apex/HTViewSolution?language=en_US&id=000003652
|
11
|
+
ip_ranges: "204.14.232.0/23,204.14.237.0/24,96.43.144.0/22,96.43.148.0/22,204.14.234.0/23,204.14.238.0/23,182.50.76.0/22"
|
12
|
+
|
13
|
+
test:
|
14
|
+
organization_id: <%= @organization_id %>
|
15
|
+
sync_enabled: false
|
16
|
+
# Salesforce owned IPs from: https://help.salesforce.com/apex/HTViewSolution?language=en_US&id=000003652
|
17
|
+
ip_ranges: "204.14.232.0/23,204.14.237.0/24,96.43.144.0/22,96.43.148.0/22,204.14.234.0/23,204.14.238.0/23,182.50.76.0/22"
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'rails/generators/active_record'
|
2
|
+
|
3
|
+
module SalesforceArSync
|
4
|
+
module Generators
|
5
|
+
class MigrationsGenerator < Rails::Generators::Base
|
6
|
+
include Rails::Generators::Migration
|
7
|
+
|
8
|
+
source_root File.expand_path('../templates', __FILE__)
|
9
|
+
|
10
|
+
desc "Generates migrations to add required columns for Salesforce sync on specified models."
|
11
|
+
|
12
|
+
#The migrate option is used to specify whether to run the migrations in the end or not
|
13
|
+
class_option :migrate, :type => :string, :banner => "[yes|no]", :lazy_default => "yes"
|
14
|
+
#The list of models to create migrations for
|
15
|
+
argument :models, :type => :array, :banner => "model1 model2 model3...", :required => true
|
16
|
+
|
17
|
+
|
18
|
+
def create_migrations
|
19
|
+
models.each do |model|
|
20
|
+
create_migration(model)
|
21
|
+
end
|
22
|
+
|
23
|
+
if options[:migrate] == "yes"
|
24
|
+
say "Performing Migrations"
|
25
|
+
run_migrations
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
protected
|
30
|
+
|
31
|
+
def run_migrations
|
32
|
+
rake("db:migrate")
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.next_migration_number(path)
|
36
|
+
unless @prev_migration_nr
|
37
|
+
@prev_migration_nr = Time.now.utc.strftime("%Y%m%d%H%M%S").to_i
|
38
|
+
else
|
39
|
+
@prev_migration_nr += 1
|
40
|
+
end
|
41
|
+
@prev_migration_nr.to_s
|
42
|
+
end
|
43
|
+
|
44
|
+
def create_migration(model_name)
|
45
|
+
#we can't load all the models in so let's assume it follows the standard nameing convention
|
46
|
+
@table_name = model_name.tableize
|
47
|
+
@model_name = model_name
|
48
|
+
|
49
|
+
migration_template "migration.rb", "db/migrate/add_salesforce_fields_to_#{@table_name}.rb"
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
class AddSalesforceFieldsTo<%= @model_name.pluralize %> < ActiveRecord::Migration
|
2
|
+
def up
|
3
|
+
add_column :<%= @table_name %>, :salesforce_id, :string, :limit => 18
|
4
|
+
add_column :<%= @table_name %>, :salesforce_updated_at, :datetime
|
5
|
+
|
6
|
+
add_index :<%= @table_name %>, :salesforce_id, :unique => true
|
7
|
+
end
|
8
|
+
|
9
|
+
def down
|
10
|
+
remove_column :<%= @table_name %>, :salesforce_id
|
11
|
+
remove_column :<%= @table_name %>, :salesforce_updated_at
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'salesforce_ar_sync/engine'
|
2
|
+
require 'salesforce_ar_sync/version'
|
3
|
+
require 'salesforce_ar_sync/extenders/salesforce_syncable'
|
4
|
+
require 'salesforce_ar_sync/salesforce_object_sync'
|
5
|
+
require 'salesforce_ar_sync/soap_handler/base'
|
6
|
+
require 'salesforce_ar_sync/soap_handler/delete'
|
7
|
+
require 'salesforce_ar_sync/ip_constraint'
|
8
|
+
|
9
|
+
module SalesforceArSync
|
10
|
+
mattr_accessor :app_root
|
11
|
+
|
12
|
+
def self.setup
|
13
|
+
yield self
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
if defined?(ActiveRecord::Base)
|
18
|
+
ActiveRecord::Base.extend SalesforceArSync::Extenders::SalesforceSyncable
|
19
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module SalesforceArSync
|
2
|
+
module Extenders
|
3
|
+
module SalesforceSyncable
|
4
|
+
def salesforce_syncable(options = {})
|
5
|
+
require 'salesforce_ar_sync/salesforce_sync'
|
6
|
+
include SalesforceArSync::SalesforceSync
|
7
|
+
|
8
|
+
self.salesforce_sync_enabled = options.has_key?(:salesforce_sync_enabled) ? options[:salesforce_sync_enabled] : true
|
9
|
+
self.salesforce_sync_attribute_mapping = options.has_key?(:sync_attributes) ? options[:sync_attributes].stringify_keys : {}
|
10
|
+
self.salesforce_async_attributes = options.has_key?(:async_attributes) ? options[:async_attributes] : {}
|
11
|
+
self.salesforce_default_attributes_for_create = options.has_key?(:default_attributes_for_create) ? options[:default_attributes_for_create] : {}
|
12
|
+
self.salesforce_id_attribute_name = options.has_key?(:salesforce_id_attribute_name) ? options[:salesforce_id_attribute_name] : :Id
|
13
|
+
self.salesforce_web_id_attribute_name = options.has_key?(:web_id_attribute_name) ? options[:web_id_attribute_name] : :WebId__c
|
14
|
+
self.salesforce_sync_web_id = options.has_key?(:salesforce_sync_web_id) ? options[:salesforce_sync_web_id] : false
|
15
|
+
self.salesforce_web_class_name = options.has_key?(:web_class_name) ? options[:web_class_name] : self.name
|
16
|
+
|
17
|
+
self.salesforce_object_name_method = options.has_key?(:salesforce_object_name) ? options[:salesforce_object_name] : nil
|
18
|
+
self.salesforce_object_method = options.has_key?(:salesforce_object) ? options[:salesforce_object] : nil
|
19
|
+
self.salesforce_skip_sync_method = options.has_key?(:except) ? options[:except] : nil
|
20
|
+
|
21
|
+
instance_eval do
|
22
|
+
before_save :salesforce_sync
|
23
|
+
after_create :sync_web_id
|
24
|
+
|
25
|
+
def salesforce_sync_web_id?
|
26
|
+
self.salesforce_sync_web_id
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
class_eval do
|
31
|
+
# Calls a method if provided to return the name of the Salesforce object the model is syncing to.
|
32
|
+
# If no method is provided, defaults to the class name
|
33
|
+
def salesforce_object_name
|
34
|
+
return send(self.class.salesforce_object_name_method) if self.class.salesforce_object_name_method.present?
|
35
|
+
return self.class.name
|
36
|
+
end
|
37
|
+
|
38
|
+
# Calls a method, if provided, to retrieve an object from Salesforce. Calls the default implementation if
|
39
|
+
# no custom method is specified
|
40
|
+
def salesforce_object
|
41
|
+
return send(self.class.salesforce_object_method) if self.class.salesforce_object_method.present?
|
42
|
+
return send(:salesforce_object_default)
|
43
|
+
end
|
44
|
+
|
45
|
+
# Calls a method, if provided, to determine if a record should be synced to Salesforce.
|
46
|
+
# The salesforce_skip_sync instance variable is also used.
|
47
|
+
# The SALESFORCE_AR_SYNC_ENABLED flag overrides all the others if set to false
|
48
|
+
def salesforce_skip_sync?
|
49
|
+
return true if SALESFORCE_AR_SYNC_CONFIG["SYNC_ENABLED"] == false
|
50
|
+
return (salesforce_skip_sync || !self.class.salesforce_sync_enabled || send(self.class.salesforce_skip_sync_method)) if self.class.salesforce_skip_sync_method.present?
|
51
|
+
return (salesforce_skip_sync || !self.class.salesforce_sync_enabled)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'ipaddr'
|
2
|
+
|
3
|
+
module SalesforceArSync
|
4
|
+
class IPConstraint
|
5
|
+
def initialize
|
6
|
+
@ip_ranges = SALESFORCE_AR_SYNC_CONFIG["IP_RANGES"]
|
7
|
+
end
|
8
|
+
|
9
|
+
def matches?(request)
|
10
|
+
if Rails.env == 'development' || Rails.env == 'test'
|
11
|
+
true
|
12
|
+
else
|
13
|
+
@ip_ranges.any?{|r| IPAddr.new(r).include?(IPAddr.new(request.remote_ip)) }
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module SalesforceArSync
|
2
|
+
# simple object to be serialized when asynchronously sending data to Salesforce
|
3
|
+
class SalesforceObjectSync < Struct.new(:web_object_name, :salesforce_object_name, :salesforce_id, :attributes)
|
4
|
+
def perform
|
5
|
+
sf_object = "Databasedotcom::#{salesforce_object_name}".constantize.find_by_Id salesforce_id
|
6
|
+
|
7
|
+
if sf_object
|
8
|
+
sf_object.update_attributes(attributes)
|
9
|
+
sf_object.reload
|
10
|
+
|
11
|
+
web_object = "#{web_object_name}".constantize.find_by_salesforce_id salesforce_id
|
12
|
+
web_object.update_attribute(:salesforce_updated_at, sf_object.SystemModstamp) unless web_object.nil?
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,197 @@
|
|
1
|
+
require 'active_support/concern'
|
2
|
+
|
3
|
+
module SalesforceArSync
|
4
|
+
module SalesforceSync
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
module ClassMethods
|
8
|
+
# Optionally holds the value to determine if salesforce syncing is enabled. Defaults to true. If set
|
9
|
+
# to false syncing will be disabled for the class
|
10
|
+
attr_accessor :salesforce_sync_enabled
|
11
|
+
|
12
|
+
# Hash mapping of Salesforce attributes to web attributes
|
13
|
+
# Example:
|
14
|
+
# { :Email => :login, :FirstName => :first_name, :LastName => :last_name }
|
15
|
+
#
|
16
|
+
# "Web" attributes can be actual method names to return a custom value
|
17
|
+
# If you are providing a method name to return a value, you should also implement a corresponding my_method_changed? to
|
18
|
+
# return if the value has changed. Otherwise it will always be synced.
|
19
|
+
attr_accessor :salesforce_sync_attribute_mapping
|
20
|
+
|
21
|
+
# Returns an array of Salesforce attributes which should be synced asynchronously
|
22
|
+
# Example: ["Last_Login_Date__c", "Login_Count__c" ]
|
23
|
+
# Note: The model will fall back to synchronous sync if non-synchronous attributes are changed along with async attributes
|
24
|
+
attr_accessor :salesforce_async_attributes
|
25
|
+
|
26
|
+
# Returns a hash of default attributes that should be used when we are creating a new record
|
27
|
+
attr_accessor :salesforce_default_attributes_for_create
|
28
|
+
|
29
|
+
# Returns the "Id" attribute of the corresponding Salesforce object
|
30
|
+
attr_accessor :salesforce_id_attribute_name
|
31
|
+
|
32
|
+
# Returns the name of the Web Objects class. A custom value can be provided if you wish
|
33
|
+
# to sync to a SF object and back to a different web object. This would generally be used
|
34
|
+
# if you wanted to flatten a web object into a larger SF object like Contact
|
35
|
+
attr_accessor :salesforce_web_class_name
|
36
|
+
|
37
|
+
attr_accessor :salesforce_web_id_attribute_name
|
38
|
+
attr_accessor :salesforce_sync_web_id
|
39
|
+
|
40
|
+
# Optionally holds the name of a method which will return the name of the Salesforce object to sync to
|
41
|
+
attr_accessor :salesforce_object_name_method
|
42
|
+
|
43
|
+
# Optionally holds the name of a method which will retrieve a Salesforce object. Default implementation is called
|
44
|
+
# if no method is specified
|
45
|
+
attr_accessor :salesforce_object_method
|
46
|
+
|
47
|
+
# Optionally holds the name of a method which can contain logic to determine if a record should be synced on save.
|
48
|
+
# If no method is given then only the salesforce_skip_sync attribute is used.
|
49
|
+
attr_accessor :salesforce_skip_sync_method
|
50
|
+
|
51
|
+
# Accepts values from an outbound message hash and will either update an existing record OR create a new record
|
52
|
+
# Firstly attempts to find an object by the salesforce_id attribute
|
53
|
+
# Secondly attempts to look an object up by it's ID (WebId__c in outbound message)
|
54
|
+
# Lastly it will create a new record setting it's salesforce_id
|
55
|
+
def salesforce_update(attributes={})
|
56
|
+
raise ArgumentError, "#{salesforce_id_attribute_name} parameter required" if attributes[salesforce_id_attribute_name].blank?
|
57
|
+
|
58
|
+
object = self.find_by_salesforce_id attributes[salesforce_id_attribute_name]
|
59
|
+
object ||= self.find_by_id attributes[salesforce_web_id_attribute_name] if salesforce_sync_web_id? && attributes[salesforce_web_id_attribute_name]
|
60
|
+
|
61
|
+
if object.nil?
|
62
|
+
object = self.new
|
63
|
+
salesforce_default_attributes_for_create.merge(:salesforce_id => attributes[salesforce_id_attribute_name]).each_pair do |k, v|
|
64
|
+
object.send("#{k}=", v)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
object.salesforce_process_update(attributes) if object && (object.salesforce_updated_at.nil? || (object.salesforce_updated_at && object.salesforce_updated_at < Time.parse(attributes[:SystemModstamp])))
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# if this instance variable is set to true, the salesforce_sync method will return without attempting
|
73
|
+
# to sync data to Salesforce
|
74
|
+
attr_accessor :salesforce_skip_sync
|
75
|
+
|
76
|
+
# Salesforce completely excludes any empty/null fields from Outbound Messages
|
77
|
+
# We initialize all declared attributes as nil before mapping the values from the message
|
78
|
+
def salesforce_empty_attributes
|
79
|
+
{}.tap do |hash|
|
80
|
+
self.class.salesforce_sync_attribute_mapping.each do |key, value|
|
81
|
+
hash[key] = nil
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
# An internal method used to get a hash of values that we are going to set from a Salesforce outbound message hash
|
87
|
+
def salesforce_attributes_to_set(attributes = {})
|
88
|
+
{}.tap do |hash|
|
89
|
+
# loop through the hash of attributes from the outbound message, and compare to our sf mappings and
|
90
|
+
# create a reversed hash of value's and key's to pass to update_attributes
|
91
|
+
attributes.each do |key, value|
|
92
|
+
# make sure our sync_mapping contains the salesforce attribute AND that our object has a setter for it
|
93
|
+
hash[self.class.salesforce_sync_attribute_mapping[key.to_s].to_sym] = value if self.class.salesforce_sync_attribute_mapping.include?(key.to_s) && self.respond_to?("#{self.class.salesforce_sync_attribute_mapping[key.to_s]}=")
|
94
|
+
end
|
95
|
+
|
96
|
+
# remove the web_id from hash if it exists, as we don't want to modify a web_id
|
97
|
+
hash.delete(:id) if hash[:id]
|
98
|
+
|
99
|
+
# update the sf_updated_at field with the system mod stamp from sf
|
100
|
+
hash[:salesforce_updated_at] = attributes[:SystemModstamp]
|
101
|
+
|
102
|
+
# incase we looked up via the WebId__c, we should set the salesforce_id
|
103
|
+
hash[:salesforce_id] = attributes[self.class.salesforce_id_attribute_name]
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
# Gets passed the Salesforce outbound message hash of changed values and updates the corresponding model
|
108
|
+
def salesforce_process_update(attributes = {})
|
109
|
+
attributes_to_update = salesforce_attributes_to_set(self.new_record? ? attributes : salesforce_empty_attributes.merge(attributes)) # only merge empty attributes for updates, so we don't overwrite the default create attributes
|
110
|
+
attributes_to_update.each_pair do |k, v|
|
111
|
+
self.send("#{k}=", v)
|
112
|
+
end
|
113
|
+
|
114
|
+
# we don't want to keep going in a endless loop. SF has just updated these values.
|
115
|
+
self.salesforce_skip_sync = true
|
116
|
+
self.save!
|
117
|
+
end
|
118
|
+
|
119
|
+
# This method will fetch the salesforce object and go looking for it if it doesn't exist
|
120
|
+
def salesforce_object_default
|
121
|
+
return @sf_object if @sf_object.present?
|
122
|
+
|
123
|
+
@sf_object = "Databasedotcom::#{self.salesforce_object_name}".constantize.send("find_by_#{self.class.salesforce_id_attribute_name.to_s}", salesforce_id) unless salesforce_id.nil?
|
124
|
+
|
125
|
+
# Look up and link object by web id
|
126
|
+
@sf_object ||= "Databasedotcom::#{self.salesforce_object_name}".constantize.send("find_by_#{self.class.salesforce_web_id_attribute_name.to_s}", id) if self.class.salesforce_sync_web_id? && !new_record?
|
127
|
+
|
128
|
+
return @sf_object
|
129
|
+
end
|
130
|
+
|
131
|
+
# Checks if the passed in attribute should be updated in Salesforce.com
|
132
|
+
def salesforce_should_update_attribute?(attribute)
|
133
|
+
!self.respond_to?("#{attribute}_changed?") || (self.respond_to?("#{attribute}_changed?") && self.send("#{attribute}_changed?"))
|
134
|
+
end
|
135
|
+
|
136
|
+
# create a hash of updates to send to salesforce
|
137
|
+
def salesforce_attributes_to_update(include_all = false)
|
138
|
+
{}.tap do |hash|
|
139
|
+
self.class.salesforce_sync_attribute_mapping.each do |key, value|
|
140
|
+
if self.respond_to?(value)
|
141
|
+
|
142
|
+
#Checkboxes in SFDC Cannot be nil. Here we check for boolean field type and set nil values to be false
|
143
|
+
attribute_value = self.send(value)
|
144
|
+
if is_boolean?(value) && attribute_value.nil?
|
145
|
+
attribute_value = false
|
146
|
+
end
|
147
|
+
|
148
|
+
hash[key] = attribute_value if include_all || salesforce_should_update_attribute?(value)
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
def is_boolean?(attribute)
|
155
|
+
self.column_for_attribute(attribute) && self.column_for_attribute(attribute).type == :boolean
|
156
|
+
end
|
157
|
+
|
158
|
+
def salesforce_create_object(attributes)
|
159
|
+
attributes.merge!(self.class.salesforce_web_id_attribute_name.to_s => id) if self.class.salesforce_sync_web_id? && !new_record?
|
160
|
+
@sf_object = "Databasedotcom::#{self.salesforce_object_name}".constantize.create(attributes)
|
161
|
+
end
|
162
|
+
|
163
|
+
def salesforce_update_object(attributes)
|
164
|
+
attributes.merge!(self.class.salesforce_web_id_attribute_name.to_s => id) if self.class.salesforce_sync_web_id? && !new_record?
|
165
|
+
salesforce_object.update_attributes(attributes) if salesforce_object
|
166
|
+
end
|
167
|
+
|
168
|
+
# if attributes specified in the async_attributes array are the only attributes being modified, then sync the data
|
169
|
+
# via delayed_job
|
170
|
+
def salesforce_perform_async_call?
|
171
|
+
return false if salesforce_attributes_to_update.empty? || self.class.salesforce_async_attributes.empty?
|
172
|
+
salesforce_attributes_to_update.keys.all? {|key| self.class.salesforce_async_attributes.include?(key) } && salesforce_id.present?
|
173
|
+
end
|
174
|
+
|
175
|
+
# sync model data to Salesforce, adding any Salesforce validation errors to the models errors
|
176
|
+
def salesforce_sync
|
177
|
+
return if self.salesforce_skip_sync?
|
178
|
+
|
179
|
+
if salesforce_perform_async_call?
|
180
|
+
Delayed::Job.enqueue(SalesforceArSync::SalesforceObjectSync.new(self.class.salesforce_web_class_name, self.salesforce_object_name, salesforce_id, salesforce_attributes_to_update), :priority => 50)
|
181
|
+
else
|
182
|
+
salesforce_update_object(salesforce_attributes_to_update) if salesforce_attributes_to_update.present? && salesforce_object
|
183
|
+
salesforce_create_object(salesforce_attributes_to_update(!new_record?)) if salesforce_id.nil? && salesforce_object.nil?
|
184
|
+
|
185
|
+
self.salesforce_id = salesforce_object.send(self.class.salesforce_id_attribute_name) unless salesforce_object.nil?
|
186
|
+
end
|
187
|
+
rescue Exception => ex
|
188
|
+
self.errors[:base] << ex.message
|
189
|
+
return false
|
190
|
+
end
|
191
|
+
|
192
|
+
def sync_web_id
|
193
|
+
return false if !self.class.salesforce_sync_web_id? || self.salesforce_skip_sync?
|
194
|
+
salesforce_object.update_attribute(self.class.salesforce_web_id_attribute_name.to_s, id) if salesforce_id && salesforce_object
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module SalesforceArSync
|
2
|
+
module SoapHandler
|
3
|
+
class Base
|
4
|
+
attr_reader :xml_hashed, :options, :sobjects
|
5
|
+
|
6
|
+
def initialize organization, options = {}
|
7
|
+
@options = options
|
8
|
+
@xml_hashed = options
|
9
|
+
@organization = SALESFORCE_AR_SYNC_CONFIG["ORGANIZATION_ID"]
|
10
|
+
@sobjects = collect_sobjects if valid?
|
11
|
+
end
|
12
|
+
|
13
|
+
# queues each individual record from the message for update
|
14
|
+
def process_notifications(priority = 90)
|
15
|
+
batch_process do |sobject|
|
16
|
+
options[:klass].camelize.constantize.delay(:priority => priority, :run_at => 5.seconds.from_now).salesforce_update(sobject)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# ensures that the received message is properly formed, and that it comes from the expected Salesforce Org
|
21
|
+
def valid?
|
22
|
+
notifications = @xml_hashed.try(:[], "Envelope").try(:[], "Body").try(:[], "notifications")
|
23
|
+
|
24
|
+
organization_id = notifications.try(:[], "OrganizationId")
|
25
|
+
return !notifications.try(:[], "Notification").nil? && organization_id == @organization # we sent this to ourselves
|
26
|
+
end
|
27
|
+
|
28
|
+
def batch_process(&block)
|
29
|
+
return if sobjects.nil? || !block_given?
|
30
|
+
sobjects.each do | sobject |
|
31
|
+
yield sobject
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
#xml for SFDC response
|
36
|
+
#called from soap_message_controller
|
37
|
+
def generate_response(error = nil)
|
38
|
+
response = "<Ack>#{sobjects.nil? ? false : true}</Ack>" unless error
|
39
|
+
if error
|
40
|
+
response = "<soapenv:Fault><faultcode>soap:Receiver</faultcode><faultstring>#{error.message}</faultstring></soapenv:Fault>"
|
41
|
+
end
|
42
|
+
return "<?xml version=\"1.0\" encoding=\"UTF-8\"?><soapenv:Envelope xmlns:soapenv=\"http://schemas.xmlsoap.org/soap/envelope/\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"><soapenv:Body><notificationsResponse>#{response}</notificationsResponse></soapenv:Body></soapenv:Envelope>"
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def collect_sobjects
|
48
|
+
notification = @xml_hashed["Envelope"]["Body"]["notifications"]["Notification"]
|
49
|
+
if notification.is_a? Array
|
50
|
+
return notification.collect{ |h| h["sObject"].symbolize_keys}
|
51
|
+
else
|
52
|
+
return [notification["sObject"].try(:symbolize_keys)]
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module SalesforceArSync
|
2
|
+
module SoapHandler
|
3
|
+
class Delete < SalesforceArSync::SoapHandler::Base
|
4
|
+
def process_notifications(priority = 90)
|
5
|
+
batch_process do |sobject|
|
6
|
+
SalesforceArSync::SoapHandler::Delete.delay(:priority => priority, :run_at => 5.seconds.from_now).delete_object(sobject)
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.delete_object(hash = {})
|
11
|
+
raise ArgumentError, "Object_Id__c parameter required" if hash[:Object_Id__c].blank?
|
12
|
+
raise ArgumentError, "Object_Type__c parameter required" if hash[:Object_Type__c].blank?
|
13
|
+
|
14
|
+
object = hash[:Object_Type__c].constantize.find_by_salesforce_id(hash[:Object_Id__c])
|
15
|
+
object.destroy if object
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
metadata
ADDED
@@ -0,0 +1,212 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: salesforce_ar_sync
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Michael Halliday
|
9
|
+
- Nick Neufeld
|
10
|
+
- Andrew Coates
|
11
|
+
- Devon Noonan
|
12
|
+
- Liam Nediger
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
date: 2012-09-17 00:00:00.000000000 Z
|
17
|
+
dependencies:
|
18
|
+
- !ruby/object:Gem::Dependency
|
19
|
+
name: rails
|
20
|
+
requirement: !ruby/object:Gem::Requirement
|
21
|
+
none: false
|
22
|
+
requirements:
|
23
|
+
- - ! '>='
|
24
|
+
- !ruby/object:Gem::Version
|
25
|
+
version: 3.1.0
|
26
|
+
type: :runtime
|
27
|
+
prerelease: false
|
28
|
+
version_requirements: !ruby/object:Gem::Requirement
|
29
|
+
none: false
|
30
|
+
requirements:
|
31
|
+
- - ! '>='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 3.1.0
|
34
|
+
- !ruby/object:Gem::Dependency
|
35
|
+
name: rake
|
36
|
+
requirement: !ruby/object:Gem::Requirement
|
37
|
+
none: false
|
38
|
+
requirements:
|
39
|
+
- - ! '>='
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
version: '0'
|
42
|
+
type: :development
|
43
|
+
prerelease: false
|
44
|
+
version_requirements: !ruby/object:Gem::Requirement
|
45
|
+
none: false
|
46
|
+
requirements:
|
47
|
+
- - ! '>='
|
48
|
+
- !ruby/object:Gem::Version
|
49
|
+
version: '0'
|
50
|
+
- !ruby/object:Gem::Dependency
|
51
|
+
name: rspec
|
52
|
+
requirement: !ruby/object:Gem::Requirement
|
53
|
+
none: false
|
54
|
+
requirements:
|
55
|
+
- - ! '>='
|
56
|
+
- !ruby/object:Gem::Version
|
57
|
+
version: '0'
|
58
|
+
type: :development
|
59
|
+
prerelease: false
|
60
|
+
version_requirements: !ruby/object:Gem::Requirement
|
61
|
+
none: false
|
62
|
+
requirements:
|
63
|
+
- - ! '>='
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
version: '0'
|
66
|
+
- !ruby/object:Gem::Dependency
|
67
|
+
name: webmock
|
68
|
+
requirement: !ruby/object:Gem::Requirement
|
69
|
+
none: false
|
70
|
+
requirements:
|
71
|
+
- - ! '>='
|
72
|
+
- !ruby/object:Gem::Version
|
73
|
+
version: '0'
|
74
|
+
type: :development
|
75
|
+
prerelease: false
|
76
|
+
version_requirements: !ruby/object:Gem::Requirement
|
77
|
+
none: false
|
78
|
+
requirements:
|
79
|
+
- - ! '>='
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '0'
|
82
|
+
- !ruby/object:Gem::Dependency
|
83
|
+
name: vcr
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
85
|
+
none: false
|
86
|
+
requirements:
|
87
|
+
- - ! '>='
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
none: false
|
94
|
+
requirements:
|
95
|
+
- - ! '>='
|
96
|
+
- !ruby/object:Gem::Version
|
97
|
+
version: '0'
|
98
|
+
- !ruby/object:Gem::Dependency
|
99
|
+
name: ammeter
|
100
|
+
requirement: !ruby/object:Gem::Requirement
|
101
|
+
none: false
|
102
|
+
requirements:
|
103
|
+
- - ~>
|
104
|
+
- !ruby/object:Gem::Version
|
105
|
+
version: 0.2.8
|
106
|
+
type: :development
|
107
|
+
prerelease: false
|
108
|
+
version_requirements: !ruby/object:Gem::Requirement
|
109
|
+
none: false
|
110
|
+
requirements:
|
111
|
+
- - ~>
|
112
|
+
- !ruby/object:Gem::Version
|
113
|
+
version: 0.2.8
|
114
|
+
- !ruby/object:Gem::Dependency
|
115
|
+
name: webmock
|
116
|
+
requirement: !ruby/object:Gem::Requirement
|
117
|
+
none: false
|
118
|
+
requirements:
|
119
|
+
- - ! '>='
|
120
|
+
- !ruby/object:Gem::Version
|
121
|
+
version: '0'
|
122
|
+
type: :development
|
123
|
+
prerelease: false
|
124
|
+
version_requirements: !ruby/object:Gem::Requirement
|
125
|
+
none: false
|
126
|
+
requirements:
|
127
|
+
- - ! '>='
|
128
|
+
- !ruby/object:Gem::Version
|
129
|
+
version: '0'
|
130
|
+
- !ruby/object:Gem::Dependency
|
131
|
+
name: supermodel
|
132
|
+
requirement: !ruby/object:Gem::Requirement
|
133
|
+
none: false
|
134
|
+
requirements:
|
135
|
+
- - ! '>='
|
136
|
+
- !ruby/object:Gem::Version
|
137
|
+
version: '0'
|
138
|
+
type: :development
|
139
|
+
prerelease: false
|
140
|
+
version_requirements: !ruby/object:Gem::Requirement
|
141
|
+
none: false
|
142
|
+
requirements:
|
143
|
+
- - ! '>='
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: '0'
|
146
|
+
- !ruby/object:Gem::Dependency
|
147
|
+
name: databasedotcom
|
148
|
+
requirement: !ruby/object:Gem::Requirement
|
149
|
+
none: false
|
150
|
+
requirements:
|
151
|
+
- - ! '>='
|
152
|
+
- !ruby/object:Gem::Version
|
153
|
+
version: '0'
|
154
|
+
type: :runtime
|
155
|
+
prerelease: false
|
156
|
+
version_requirements: !ruby/object:Gem::Requirement
|
157
|
+
none: false
|
158
|
+
requirements:
|
159
|
+
- - ! '>='
|
160
|
+
- !ruby/object:Gem::Version
|
161
|
+
version: '0'
|
162
|
+
description: ActiveRecord extension & rails engine for syncing data with Salesforce.com
|
163
|
+
email:
|
164
|
+
- mhalliday@infotech.com
|
165
|
+
- nneufeld@infotech.com
|
166
|
+
- acoates@infotech.com
|
167
|
+
- dnoonan@infotech.com
|
168
|
+
- lnediger@infotech.com.com
|
169
|
+
executables: []
|
170
|
+
extensions: []
|
171
|
+
extra_rdoc_files: []
|
172
|
+
files:
|
173
|
+
- README.md
|
174
|
+
- LICENSE
|
175
|
+
- lib/generators/salesforce_ar_sync/configuration/configuration_generator.rb
|
176
|
+
- lib/generators/salesforce_ar_sync/configuration/templates/salesforce_ar_sync.yml
|
177
|
+
- lib/generators/salesforce_ar_sync/migrations/migrations_generator.rb
|
178
|
+
- lib/generators/salesforce_ar_sync/migrations/templates/migration.rb
|
179
|
+
- lib/salesforce_ar_sync/engine.rb
|
180
|
+
- lib/salesforce_ar_sync/extenders/salesforce_syncable.rb
|
181
|
+
- lib/salesforce_ar_sync/ip_constraint.rb
|
182
|
+
- lib/salesforce_ar_sync/salesforce_object_sync.rb
|
183
|
+
- lib/salesforce_ar_sync/salesforce_sync.rb
|
184
|
+
- lib/salesforce_ar_sync/soap_handler/base.rb
|
185
|
+
- lib/salesforce_ar_sync/soap_handler/delete.rb
|
186
|
+
- lib/salesforce_ar_sync/version.rb
|
187
|
+
- lib/salesforce_ar_sync.rb
|
188
|
+
homepage: http://github.com/InfoTech/
|
189
|
+
licenses: []
|
190
|
+
post_install_message:
|
191
|
+
rdoc_options: []
|
192
|
+
require_paths:
|
193
|
+
- lib
|
194
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
195
|
+
none: false
|
196
|
+
requirements:
|
197
|
+
- - ! '>='
|
198
|
+
- !ruby/object:Gem::Version
|
199
|
+
version: '0'
|
200
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
201
|
+
none: false
|
202
|
+
requirements:
|
203
|
+
- - ! '>='
|
204
|
+
- !ruby/object:Gem::Version
|
205
|
+
version: '0'
|
206
|
+
requirements: []
|
207
|
+
rubyforge_project:
|
208
|
+
rubygems_version: 1.8.24
|
209
|
+
signing_key:
|
210
|
+
specification_version: 3
|
211
|
+
summary: ActiveRecord extension & rails engine for syncing data with Salesforce.com
|
212
|
+
test_files: []
|