netsuite_rails 0.2.2 → 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (34) hide show
  1. checksums.yaml +15 -0
  2. data/Gemfile +4 -2
  3. data/README.md +186 -10
  4. data/circle.yml +3 -0
  5. data/lib/netsuite_rails/configuration.rb +7 -5
  6. data/lib/netsuite_rails/netsuite_rails.rb +24 -4
  7. data/lib/netsuite_rails/poll_trigger.rb +10 -9
  8. data/lib/netsuite_rails/record_sync/poll_manager.rb +23 -11
  9. data/lib/netsuite_rails/record_sync/pull_manager.rb +2 -0
  10. data/lib/netsuite_rails/record_sync/push_manager.rb +76 -38
  11. data/lib/netsuite_rails/record_sync.rb +28 -11
  12. data/lib/netsuite_rails/routines/company_contact_match.rb +98 -0
  13. data/lib/netsuite_rails/spec/disabler.rb +27 -0
  14. data/lib/netsuite_rails/spec/query_helpers.rb +93 -0
  15. data/lib/netsuite_rails/spec/spec_helper.rb +2 -79
  16. data/lib/netsuite_rails/sync_trigger.rb +40 -17
  17. data/lib/netsuite_rails/tasks/netsuite.rb +33 -4
  18. data/lib/netsuite_rails/transformations.rb +59 -19
  19. data/lib/netsuite_rails/url_helper.rb +45 -12
  20. data/netsuite_rails.gemspec +2 -2
  21. data/spec/models/configuration_spec.rb +11 -0
  22. data/spec/models/poll_manager_spec.rb +11 -2
  23. data/spec/models/poll_trigger_spec.rb +31 -11
  24. data/spec/models/record_sync/push_manager_spec.rb +51 -0
  25. data/spec/models/record_sync_spec.rb +16 -0
  26. data/spec/models/spec_helper_spec.rb +1 -2
  27. data/spec/models/transformations_spec.rb +62 -0
  28. data/spec/models/url_helper_spec.rb +20 -9
  29. data/spec/spec_helper.rb +19 -0
  30. data/spec/support/example_models.rb +33 -1
  31. metadata +19 -25
  32. data/.travis.yml +0 -3
  33. data/lib/netsuite_rails/netsuite_configure.rb +0 -14
  34. data/spec/support/netsuite_rails.rb +0 -1
checksums.yaml ADDED
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ YTVkYzk2NDQyNzI5MGZjODkxOTM4MWFkYjExYTNlZWZkNGFjNjFmMQ==
5
+ data.tar.gz: !binary |-
6
+ NjdmOWIxMmE0MjNkYjE2MTU2ZDBmMzg4MDViNWZjMzdmZGIwZjgzOQ==
7
+ SHA512:
8
+ metadata.gz: !binary |-
9
+ OWJlMzFhMGJhNTM1NGZkMjRmMGMwNmU2ZmUzZTFhODQxMGQzYjZhMWNmMjE2
10
+ M2NlYjgwMTc0MWU4Y2FhMTBjNWVjZGM0NzU2NWIxNDY1NjMzYTE2N2ZlYWM4
11
+ N2NkOTY2ZTJhNzBiMDg2NjY0MTY4ZWYyYmMxNjk4NDc3NzQ5NjQ=
12
+ data.tar.gz: !binary |-
13
+ MDE5NmE5OWZmYjhjYzIyMGQ3ZmFmMzljZDc1ZTkwZDQ1MjRjYjUyZTk4MjAx
14
+ YjI2NTM1YmZmNWE5NjE0MGZhMzg1NmZlOTA5ZDA0NWU0OWU2ODgyNmM4NzMx
15
+ YzUxOTY5ODQ1MDQ0YWE3NTNkY2YwYjA1NmY4YThlNjUyNTgwMDc=
data/Gemfile CHANGED
@@ -8,6 +8,8 @@ group :test do
8
8
  # gem 'rack-test'
9
9
  # gem 'webmock'
10
10
 
11
+ gem 'simplecov', :require => false
12
+
11
13
  gem 'faker'
12
14
  gem 'shoulda-matchers'
13
15
  gem 'rails', '3.2.16'
@@ -15,7 +17,7 @@ group :test do
15
17
 
16
18
  gem 'rspec-rails', '~> 3.1'
17
19
  gem 'pry-nav'
18
-
20
+
19
21
  gem 'rerun'
20
22
  gem 'rb-fsevent'
21
- end
23
+ end
data/README.md CHANGED
@@ -1,10 +1,90 @@
1
- [![Build Status](https://travis-ci.org/NetSweet/netsuite_rails.svg?branch=master)](https://travis-ci.org/NetSweet/netsuite_rails)
1
+ [![Circle CI](https://circleci.com/gh/NetSweet/netsuite_rails.svg?style=svg)](https://circleci.com/gh/NetSweet/netsuite_rails)
2
+ [![Slack Status](https://opensuite-slackin.herokuapp.com/badge.svg)](http://opensuite-slackin.herokuapp.com)
2
3
 
3
4
  # NetSuite Rails
4
5
 
5
- **Note:** Documentation is horrible... look at the code for details.
6
+ **<span style="color: red">Note:</span>** Documentation is horrible: PRs welcome. Look at the code for details.
6
7
 
7
- Build custom Ruby on Rails applications that sync to NetSuite.
8
+ Build Ruby on Rails applications that sync ActiveRecord (ActiveModel and plain old ruby objects too) in real-time to NetSuite. Here's an example:
9
+
10
+ ```ruby
11
+ class Item < ActiveRecord::Base
12
+ include NetSuiteRails::RecordSync
13
+
14
+ # specify the NS record that your rails model maps to
15
+ netsuite_record_class NetSuite::Records::InventoryItem
16
+
17
+ netsuite_sync :read_write,
18
+ # specify the frequency that your app should poll NetSuite for updates
19
+ frequency: 1.day,
20
+ # it's possible to base syncing off of a saved search. Be sure that "Internal ID" is one of your search result columns
21
+ saved_search_id: 123,
22
+ # limit pushing to NetSuite based on conditional
23
+ if: -> { self.a_condition? },
24
+ # limit pulling from NetSuite based on conditional. This is only
25
+ # considered when handling a single pull
26
+ pull_if: -> { self.another_condition? },
27
+
28
+ # accepted values are :async and :sync. Default is :async
29
+ mode: :sync
30
+
31
+
32
+ # local => remote field mapping
33
+ netsuite_field_map({
34
+ :item_number => :item_id,
35
+ :name => :display_name,
36
+
37
+ # the corresponding NetSuite field must be manually specified in before_netsuite_push
38
+ :user => Proc.new do |local_rails_record, netsuite_record, sync_direction|
39
+ if direction == :pull
40
+
41
+ elsif direction == :push
42
+
43
+ end
44
+ end,
45
+
46
+ :custom_field_list => {
47
+ :a_local_field => :custrecord_remote_field
48
+ :a_special_local_field => Proc.new do |local, ns_record, direction|
49
+ if direction == :push
50
+ # if proc is used with a field mapping, the field must be specified in `netsuite_manual_fields`
51
+ ns_record.custom_field_list.custentity_special_long = 1
52
+ ns_record.custom_field_list.custentity_special_long.type = 'platformCore:LongCustomFieldRef'
53
+ end
54
+ end
55
+ }
56
+ })
57
+
58
+ # sanitizes input from rails to ensure NS doesn't throw a fatal error
59
+ netsuite_field_hints({
60
+ :phone => :phone,
61
+ :email => :email
62
+ })
63
+
64
+ before_netsuite_push do |netsuite_record|
65
+ self.netsuite_manual_fields = [ :entity, :custom_field_list ]
66
+ end
67
+ end
68
+ ```
69
+
70
+ Your ruby model:
71
+
72
+ * Needs to have a `netsuite_id` and `netsuite_id=` method
73
+ * Does not need to be an `ActiveRecord` model. If you don't use ActiveRecord it is your responsibility
74
+ to trigger `Model#netsuite_push`.
75
+
76
+ Notes:
77
+
78
+ * If `sync_mode == :async` `model.save` will be run if a record is created referencing an existing NetSuite object: `model.create! netsuite_id: 123`
79
+ * If you are using `update`, a `update` call will not be run if no changed fields are detected. If you are manually using fields specify them with `netsuite_manual_fields`
80
+
81
+ ## Using Upsert
82
+
83
+ TODO generating external ID tag
84
+
85
+ TODO configuring upsert
86
+
87
+ TODO add vs upsert consideration
8
88
 
9
89
  ## Installation
10
90
 
@@ -12,43 +92,121 @@ Build custom Ruby on Rails applications that sync to NetSuite.
12
92
  gem 'netsuite_rails'
13
93
  ```
14
94
 
15
- Install the database migration for poll timestamps
95
+ Install the database migration to persist poll timestamps:
16
96
 
17
97
  ```bash
18
98
  rails g netsuite_rails:install
19
99
  ```
20
100
 
21
- ### Date & Time
101
+ This helps netsuite_rails to know when the last time your rails DB was synced with the NS.
102
+
103
+ ## Date
104
+
105
+
106
+ ## Time
107
+
108
+ "Time of Day" fields in NetSuite are especially tricky. To ensure that times don't shift when you push them to NetSuite here are some tips:
109
+
110
+ 1. Take a look at the company time zone setup. This is in Setup
111
+ 2. Ensure your WebService's Employee record has either:
112
+ * No time zone set
113
+ * The same time zone as the company
114
+ 3. Ensure that the WebService's GUI preferences have the same time zone settings as the company. This effects how times are translated via SuiteTalk.
115
+ 4. Set the `netsuite_instance_time_zone_offset` setting to your company's time zone
22
116
 
23
117
  ```ruby
24
118
  # set your timezone offset
25
119
  NetSuiteRails::Configuration.netsuite_instance_time_zone_offset(-6)
26
120
  ```
27
121
 
122
+ ### Changing WebService User's TimeZone Preferences
123
+
124
+ It might take a couple hours for time zone changes to take effect. [From my experience](http://mikebian.co/netsuite-suitetalk-user-role-edits-are-delayed/), either the time zone changes have some delay associated with them or the time zone implementation is extremely buggy.
125
+
28
126
  ## Usage
29
127
 
30
- modes: :read, :read_write, :aggressive
128
+ ### Syncing Options
129
+
130
+ ```
131
+ netsuite_record_class NetSuite::Records::Customer
132
+ netsuite_record_class NetSuite::Records::CustomRecord, 123
133
+
134
+ netsuite_sync: :read
135
+ netsuite_sync: :read_write
136
+ # TODO not after_netsuite_push replacement for aggressive sync
137
+
138
+ netsuite_sync: :read, frequency: :never
139
+ netsuite_sync: :read, frequency: 5.minutes
140
+ netsuite_sync: :read, if: -> { self.condition_met? }
141
+
142
+ ```
31
143
 
32
144
  When using a proc in a NS mapping, you are responsible for setting local and remote values
33
145
 
146
+ The default sync frequency is [one day](https://github.com/NetSweet/netsuite_rails/blob/c453326a4190e68a2fd9d7690b2b1f2f105ec8b9/lib/netsuite_rails/poll_trigger.rb#L27).
147
+
34
148
  for pushing tasks to DJ https://github.com/collectiveidea/delayed_job/wiki/Rake-Task-as-a-Delayed-Job
35
149
 
36
150
  `:if` for controlling when syncing occurs
37
151
 
38
- TODO hooks for before/after push/pull
152
+ Easily disable/enable syncing via env vars:
153
+
154
+ ```ruby
155
+ NetSuiteRails.configure do
156
+ netsuite_pull_disabled ENV['NETSUITE_PULL_DISABLED'].present? && ENV['NETSUITE_PULL_DISABLED'] == "true"
157
+ netsuite_push_disabled ENV['NETSUITE_PUSH_DISABLED'].present? && ENV['NETSUITE_PUSH_DISABLED'] == "true"
158
+
159
+ if ENV['NETSUITE_DISABLE_SYNC'].present? && ENV['NETSUITE_DISABLE_SYNC'] == "true"
160
+ netsuite_pull_disabled true
161
+ netsuite_push_disabled true
162
+ end
163
+ end
164
+
165
+ ```
166
+
167
+ ### Hooks
168
+
169
+ ```ruby
170
+ # the netsuite record is passed a single argument to this block (or method reference)
171
+ # this provides the opportunity to set custom fields or run custom logic to prepare
172
+ # the record for the NetSuite envoirnment
173
+ before_netsuite_push
174
+ after_netsuite_push
175
+
176
+ # netsuite_pulling? is true when this callback is executed
177
+ after_netsuite_pull
178
+ ```
39
179
 
40
- ### Syncing
180
+ ### Rake Tasks for Syncing
41
181
 
42
182
  ```bash
183
+ # update & create local records modified in netsuite sync the last sync time
43
184
  rake netsuite:sync
44
185
 
186
+ # pull all records in NetSuite and update/create local records
45
187
  rake netsuite:fresh_sync
188
+
189
+ # only update records that have already been synced
190
+ rake netsuite:sync_local RECORD_MODELS=YourModel LIST_MODELS=YourListModel
46
191
  ```
47
192
 
48
193
  Caveats:
49
194
 
50
195
  * If you have date time fields, or custom fields that will trigger `changed_attributes` this might cause issues when pulling an existing record
51
- * `changed_attributes` doesn't work well with store
196
+ * `changed_attributes` doesn't work well with `store`s
197
+
198
+ ### Delayed Job
199
+
200
+ The more records that use netsuite_rails, the longer you'll need your job timeout to be:
201
+
202
+ ```ruby
203
+ # config/initializers/delayed_job.rb
204
+ Delayed::Worker.max_run_time = 80.minutes
205
+ ```
206
+
207
+ ## Non-AR Backed Model
208
+
209
+ Implement `changed_attributes` in your non-AR backed model
52
210
 
53
211
  ## Testing
54
212
 
@@ -57,6 +215,24 @@ Caveats:
57
215
  require 'netsuite_rails/spec/spec_helper'
58
216
  ```
59
217
 
218
+ # Syncing Using Rake Tasks
219
+
220
+ ```ruby
221
+ # clockwork.rb
222
+ every(1.minutes, 'netsuite sync') {
223
+ # prevent multiple netsuite:sync DJ commands from being added; only one is needed in the queue at a time
224
+ unless Delayed::Job.where(failed_at: nil, locked_by: nil).detect { |j| j.payload_object.class == DelayedRake && j.payload_object.task == 'netsuite:sync'}
225
+ Delayed::Job.enqueue DelayedRake.new("netsuite:sync")
226
+ end
227
+ }
228
+
229
+ # schedule.rb
230
+ # DelayedRake: https://github.com/collectiveidea/delayed_job/wiki/Rake-Task-as-a-Delayed-Job
231
+ every 2.minutes do
232
+ runner 'Delayed::Job.enqueue(DelayedRake.new("netsuite:sync"),priority:1,run_at: Time.now);'
233
+ end
234
+ ```
235
+
60
236
  ## Author
61
237
 
62
- * Michael Bianco @iloveitaly
238
+ * Michael Bianco @iloveitaly
data/circle.yml ADDED
@@ -0,0 +1,3 @@
1
+ database:
2
+ override:
3
+ - echo "no database setup"
@@ -14,7 +14,7 @@ module NetSuiteRails
14
14
 
15
15
  def netsuite_sync_mode(mode = nil)
16
16
  if mode.nil?
17
- attributes[:sync_mode] ||= :none
17
+ attributes[:sync_mode] ||= :async
18
18
  else
19
19
  attributes[:sync_mode] = mode
20
20
  end
@@ -22,17 +22,19 @@ module NetSuiteRails
22
22
 
23
23
  def netsuite_push_disabled(flag = nil)
24
24
  if flag.nil?
25
- attributes[:flag] ||= false
25
+ attributes[:push_disabled] = false if attributes[:push_disabled].nil?
26
+ attributes[:push_disabled]
26
27
  else
27
- attributes[:flag] = flag
28
+ attributes[:push_disabled] = flag
28
29
  end
29
30
  end
30
31
 
31
32
  def netsuite_pull_disabled(flag = nil)
32
33
  if flag.nil?
33
- attributes[:flag] ||= false
34
+ attributes[:pull_disabled] = false if attributes[:pull_disabled].nil?
35
+ attributes[:pull_disabled]
34
36
  else
35
- attributes[:flag] = flag
37
+ attributes[:pull_disabled] = flag
36
38
  end
37
39
  end
38
40
 
@@ -14,6 +14,8 @@ require 'netsuite_rails/record_sync/poll_manager'
14
14
  require 'netsuite_rails/record_sync/pull_manager'
15
15
  require 'netsuite_rails/record_sync/push_manager'
16
16
 
17
+ require 'netsuite_rails/routines/company_contact_match'
18
+
17
19
  require 'netsuite_rails/list_sync'
18
20
  require 'netsuite_rails/list_sync/poll_manager'
19
21
 
@@ -23,14 +25,32 @@ module NetSuiteRails
23
25
  Rails::VERSION::MAJOR >= 4
24
26
  end
25
27
 
28
+ def self.configure_from_env(&block)
29
+ self.configure do
30
+ reset!
31
+
32
+ netsuite_pull_disabled ENV['NETSUITE_PULL_DISABLED'].present? && ENV['NETSUITE_PULL_DISABLED'] == "true"
33
+ netsuite_push_disabled ENV['NETSUITE_PUSH_DISABLED'].present? && ENV['NETSUITE_PUSH_DISABLED'] == "true"
34
+
35
+ if ENV['NETSUITE_DISABLE_SYNC'].present? && ENV['NETSUITE_DISABLE_SYNC'] == "true"
36
+ netsuite_pull_disabled true
37
+ netsuite_push_disabled true
38
+ end
39
+
40
+ polling_page_size if ENV['NETSUITE_POLLING_PAGE_SIZE'].present?
41
+ end
42
+
43
+ self.configure(&block) if block
44
+ end
45
+
46
+ def self.configure(&block)
47
+ NetSuiteRails::Configuration.instance_eval(&block)
48
+ end
49
+
26
50
  class Railtie < ::Rails::Railtie
27
51
  rake_tasks do
28
52
  load 'netsuite_rails/tasks/netsuite.rb'
29
53
  end
30
-
31
- config.before_configuration do
32
- require 'netsuite_rails/netsuite_configure'
33
- end
34
54
  end
35
55
 
36
56
  end
@@ -27,32 +27,33 @@ module NetSuiteRails
27
27
  sync_frequency = klass.netsuite_sync_options[:frequency] || 1.day
28
28
 
29
29
  if sync_frequency == :never
30
- Rails.logger.info "Not syncing #{klass.to_s}"
30
+ Rails.logger.info "NetSuite: Not syncing #{klass.to_s}"
31
31
  next
32
32
  end
33
33
 
34
- Rails.logger.info "NetSuite: Syncing #{klass.to_s}"
35
-
36
- preference = PollTimestamp.for_class(klass)
34
+ last_class_poll = PollTimestamp.for_class(klass)
35
+ poll_execution_time = DateTime.now
37
36
 
38
37
  # check if we've never synced before
39
- if preference.new_record?
38
+ if last_class_poll.new_record?
39
+ Rails.logger.info "NetSuite: Syncing #{klass} for the first time"
40
40
  klass.netsuite_poll({ import_all: true }.merge(opts))
41
41
  else
42
42
  # TODO look into removing the conditional parsing; I don't think this is needed
43
- last_poll_date = preference.value
43
+ last_poll_date = last_class_poll.value
44
44
  last_poll_date = DateTime.parse(last_poll_date) unless last_poll_date.is_a?(DateTime)
45
45
 
46
46
  if DateTime.now.to_i - last_poll_date.to_i > sync_frequency
47
- Rails.logger.info "NetSuite: Syncing #{klass} modified since #{last_poll_date}"
47
+ Rails.logger.info "NetSuite: #{klass} is due to be synced, last checked #{last_poll_date}"
48
48
  klass.netsuite_poll({ last_poll: last_poll_date }.merge(opts))
49
49
  else
50
50
  Rails.logger.info "NetSuite: Skipping #{klass} because of syncing frequency"
51
+ next
51
52
  end
52
53
  end
53
54
 
54
- preference.value = DateTime.now
55
- preference.save!
55
+ last_class_poll.value = poll_execution_time
56
+ last_class_poll.save!
56
57
  end
57
58
  end
58
59
 
@@ -19,7 +19,7 @@ module NetSuiteRails
19
19
  end
20
20
 
21
21
  unless netsuite_batch
22
- binding.pry
22
+ raise "NetSuite: #{klass}. Error running NS search. No Netsuite batch found. Most likely a search timeout."
23
23
  end
24
24
 
25
25
  netsuite_batch.each do |netsuite_record|
@@ -35,12 +35,14 @@ module NetSuiteRails
35
35
  }.merge(opts)
36
36
 
37
37
  opts[:netsuite_record_class] ||= klass.netsuite_record_class
38
+ opts[:netsuite_custom_record_type_id] ||= klass.netsuite_custom_record_type_id if opts[:netsuite_record_class] == NetSuite::Records::CustomRecord
38
39
  opts[:saved_search_id] ||= klass.netsuite_sync_options[:saved_search_id]
40
+ opts[:body_fields_only] ||= false
39
41
 
40
42
  search = opts[:netsuite_record_class].search(
41
43
  poll_criteria(klass, opts).merge({
42
44
  preferences: {
43
- body_fields_only: false,
45
+ body_fields_only: opts[:body_fields_only],
44
46
  page_size: opts[:page_size]
45
47
  }
46
48
  })
@@ -48,7 +50,7 @@ module NetSuiteRails
48
50
 
49
51
  # TODO more robust error reporting
50
52
  unless search
51
- raise 'error running netsuite sync'
53
+ raise "NetSuite: #{klass}. Error running NS search. Most likely a search timeout."
52
54
  end
53
55
 
54
56
  process_search_results(klass, opts, search)
@@ -90,7 +92,7 @@ module NetSuiteRails
90
92
 
91
93
  if opts[:netsuite_record_class] == NetSuite::Records::CustomRecord
92
94
  opts[:netsuite_custom_record_type_id] ||= klass.netsuite_custom_record_type_id
93
-
95
+
94
96
  criteria << {
95
97
  field: 'recType',
96
98
  operator: 'is',
@@ -116,14 +118,14 @@ module NetSuiteRails
116
118
  full_record_data: -1,
117
119
  }.merge(opts)
118
120
 
119
- Rails.logger.info "NetSuite: Processing #{search.total_records} over #{search.total_pages} pages"
120
-
121
121
  # TODO need to improve the conditional here to match the get_list call conditional belo
122
122
  if opts[:import_all] && opts[:skip_existing]
123
123
  synced_netsuite_list = klass.pluck(:netsuite_id)
124
124
  end
125
-
125
+
126
126
  search.results_in_batches do |batch|
127
+ Rails.logger.info "NetSuite: Syncing #{klass}. Current Page: #{search.current_page}. Processing #{search.total_records} over #{search.total_pages} pages."
128
+
127
129
  # a saved search is processed as a advanced search; advanced search often does not allow you to retrieve
128
130
  # all of the fields (ex: addressbooklist on customer) that a normal search does
129
131
  # the only way to get those fields is to pull down the full record again using getAll
@@ -136,7 +138,16 @@ module NetSuiteRails
136
138
  end
137
139
 
138
140
  if filtered_netsuite_id_list.present?
139
- opts[:netsuite_record_class].get_list(list: filtered_netsuite_id_list)
141
+ Rails.logger.info "NetSuite: Syncing #{klass}. Running get_list for #{filtered_netsuite_id_list.length} records"
142
+
143
+ if opts[:netsuite_record_class] == NetSuite::Records::CustomRecord
144
+ NetSuite::Records::CustomRecord.get_list(
145
+ list: filtered_netsuite_id_list,
146
+ type_id: opts[:netsuite_custom_record_type_id]
147
+ )
148
+ else
149
+ opts[:netsuite_record_class].get_list(list: filtered_netsuite_id_list)
150
+ end
140
151
  else
141
152
  []
142
153
  end
@@ -164,7 +175,8 @@ module NetSuiteRails
164
175
  end
165
176
 
166
177
  def needs_get_list?(opts)
167
- (opts[:saved_search_id].present? && opts[:full_record_data] != false) || opts[:full_record_data] == true
178
+ (opts[:saved_search_id].present? && opts[:full_record_data] != false) ||
179
+ opts[:full_record_data] == true
168
180
  end
169
181
 
170
182
  # TODO this should remain in the pull manager
@@ -180,7 +192,7 @@ module NetSuiteRails
180
192
 
181
193
  custom_field_value
182
194
  end
183
-
195
+
184
196
  end
185
197
  end
186
- end
198
+ end
@@ -3,6 +3,8 @@ module NetSuiteRails
3
3
  module PullManager
4
4
  extend self
5
5
 
6
+ # TODO pull relevant methods out of poll manager and into this class
7
+
6
8
  def extract_custom_field_value(custom_field_value)
7
9
  if custom_field_value.present? && custom_field_value.is_a?(Hash) && custom_field_value.has_key?(:name)
8
10
  custom_field_value = custom_field_value[:name]