netsuite_rails 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. data/.gitignore +3 -0
  2. data/.travis.yml +3 -0
  3. data/Gemfile +17 -7
  4. data/README.md +9 -4
  5. data/lib/netsuite_rails/configuration.rb +3 -1
  6. data/lib/netsuite_rails/list_sync/poll_manager.rb +30 -0
  7. data/lib/netsuite_rails/list_sync.rb +2 -2
  8. data/lib/netsuite_rails/netsuite_rails.rb +13 -3
  9. data/lib/netsuite_rails/poll_timestamp.rb +5 -0
  10. data/lib/netsuite_rails/{poll_manager.rb → poll_trigger.rb} +18 -3
  11. data/lib/netsuite_rails/record_sync/poll_manager.rb +186 -0
  12. data/lib/netsuite_rails/record_sync/pull_manager.rb +10 -137
  13. data/lib/netsuite_rails/record_sync/push_manager.rb +59 -21
  14. data/lib/netsuite_rails/record_sync.rb +142 -140
  15. data/lib/netsuite_rails/spec/spec_helper.rb +34 -6
  16. data/lib/netsuite_rails/sync_trigger.rb +80 -75
  17. data/lib/netsuite_rails/tasks/netsuite.rb +26 -30
  18. data/lib/netsuite_rails/url_helper.rb +12 -3
  19. data/netsuite_rails.gemspec +3 -3
  20. data/spec/models/poll_manager_spec.rb +45 -0
  21. data/spec/models/poll_trigger_spec.rb +26 -0
  22. data/spec/models/record_sync/push_manager_spec.rb +0 -0
  23. data/spec/models/spec_helper_spec.rb +29 -0
  24. data/spec/models/sync_trigger_spec.rb +62 -0
  25. data/spec/models/url_helper_spec.rb +23 -0
  26. data/spec/spec_helper.rb +17 -1
  27. data/spec/support/config/database.yml +11 -0
  28. data/spec/support/dynamic_models/class_builder.rb +48 -0
  29. data/spec/support/dynamic_models/model_builder.rb +83 -0
  30. data/spec/support/example_models.rb +31 -0
  31. data/spec/support/netsuite_rails.rb +1 -0
  32. data/spec/support/test_application.rb +55 -0
  33. metadata +36 -10
  34. data/lib/netsuite_rails/list_sync/pull_manager.rb +0 -33
data/.gitignore CHANGED
@@ -1,2 +1,5 @@
1
+ db/
2
+ tmp/
3
+ .bundle/
1
4
  Gemfile.lock
2
5
  vendor/
data/.travis.yml ADDED
@@ -0,0 +1,3 @@
1
+ notifications:
2
+ email: false
3
+ script: bundle exec rspec
data/Gemfile CHANGED
@@ -2,10 +2,20 @@ source 'https://rubygems.org'
2
2
  gemspec
3
3
 
4
4
  # TODO would like to support easy NetSuite VCR recording in the future
5
- # group :test do
6
- # gem 'vcr'
7
- # gem 'rspec', '~> 2.14'
8
- # gem 'rack-test'
9
- # gem 'webmock'
10
- # gem 'shoulda-matchers'
11
- # end
5
+ group :test do
6
+ # gem 'vcr'
7
+ # gem 'rspec', '~> 2.14'
8
+ # gem 'rack-test'
9
+ # gem 'webmock'
10
+
11
+ gem 'faker'
12
+ gem 'shoulda-matchers'
13
+ gem 'rails', '3.2.16'
14
+ gem 'sqlite3', :platform => :ruby
15
+
16
+ gem 'rspec-rails', '~> 3.1'
17
+ gem 'pry-nav'
18
+
19
+ gem 'rerun'
20
+ gem 'rb-fsevent'
21
+ end
data/README.md CHANGED
@@ -1,8 +1,10 @@
1
+ [![Build Status](https://travis-ci.org/NetSweet/netsuite_rails.svg?branch=master)](https://travis-ci.org/NetSweet/netsuite_rails)
2
+
1
3
  # NetSuite Rails
2
4
 
3
5
  **Note:** Documentation is horrible... look at the code for details.
4
6
 
5
- Build custom rails application that sync to NetSuite.
7
+ Build custom Ruby on Rails applications that sync to NetSuite.
6
8
 
7
9
  ## Installation
8
10
 
@@ -19,9 +21,8 @@ rails g netsuite_rails:install
19
21
  ### Date & Time
20
22
 
21
23
  ```ruby
22
- NetSuiteRails.configure do
23
-
24
- end
24
+ # set your timezone offset
25
+ NetSuiteRails::Configuration.netsuite_instance_time_zone_offset(-6)
25
26
  ```
26
27
 
27
28
  ## Usage
@@ -32,6 +33,10 @@ When using a proc in a NS mapping, you are responsible for setting local and rem
32
33
 
33
34
  for pushing tasks to DJ https://github.com/collectiveidea/delayed_job/wiki/Rake-Task-as-a-Delayed-Job
34
35
 
36
+ `:if` for controlling when syncing occurs
37
+
38
+ TODO hooks for before/after push/pull
39
+
35
40
  ### Syncing
36
41
 
37
42
  ```bash
@@ -2,6 +2,8 @@ module NetSuiteRails
2
2
  module Configuration
3
3
  extend self
4
4
 
5
+ NETSUITE_MAX_PAGE_SIZE = 1000
6
+
5
7
  def reset!
6
8
  attributes.clear
7
9
  end
@@ -44,7 +46,7 @@ module NetSuiteRails
44
46
 
45
47
  def polling_page_size(size = nil)
46
48
  if size.nil?
47
- attributes[:size] ||= 1000
49
+ attributes[:size] ||= NETSUITE_MAX_PAGE_SIZE
48
50
  else
49
51
  attributes[:size] = size
50
52
  end
@@ -0,0 +1,30 @@
1
+ module NetSuiteRails
2
+ module ListSync
3
+ module PollManager
4
+ extend self
5
+
6
+ def poll(klass, opts = {})
7
+ custom_list = NetSuite::Records::CustomList.get(klass.netsuite_list_id)
8
+
9
+ process_results(klass, opts, custom_list.custom_value_list.custom_value)
10
+ end
11
+
12
+ def process_results(klass, opts, list)
13
+ list.each do |custom_value|
14
+ local_record = klass.where(netsuite_id: custom_value.attributes[:value_id]).first_or_initialize
15
+
16
+ if local_record.respond_to?(:value=)
17
+ local_record.value = custom_value.attributes[:value]
18
+ end
19
+
20
+ if local_record.respond_to?(:inactive=)
21
+ local_record.inactive = custom_value.attributes[:is_inactive]
22
+ end
23
+
24
+ local_record.save!
25
+ end
26
+ end
27
+
28
+ end
29
+ end
30
+ end
@@ -4,7 +4,7 @@ module NetSuiteRails
4
4
  def self.included(klass)
5
5
  klass.send(:extend, ClassMethods)
6
6
 
7
- PollManager.attach(klass)
7
+ PollTrigger.attach(klass)
8
8
  end
9
9
 
10
10
  module ClassMethods
@@ -17,7 +17,7 @@ module NetSuiteRails
17
17
  end
18
18
 
19
19
  def netsuite_poll(opts = {})
20
- NetSuiteRails::ListSync::PullManager.poll(self, opts)
20
+ NetSuiteRails::ListSync::PollManager.poll(self, opts)
21
21
  end
22
22
  end
23
23
 
@@ -1,18 +1,28 @@
1
+ require 'netsuite'
2
+
1
3
  require 'netsuite_rails/configuration'
2
4
  require 'netsuite_rails/poll_timestamp'
3
5
  require 'netsuite_rails/transformations'
4
- require 'netsuite_rails/poll_manager'
6
+ require 'netsuite_rails/url_helper'
7
+
8
+ require 'netsuite_rails/poll_trigger'
5
9
  require 'netsuite_rails/sync_trigger'
6
10
  require 'netsuite_rails/sub_list_sync'
11
+
7
12
  require 'netsuite_rails/record_sync'
13
+ require 'netsuite_rails/record_sync/poll_manager'
8
14
  require 'netsuite_rails/record_sync/pull_manager'
9
15
  require 'netsuite_rails/record_sync/push_manager'
16
+
10
17
  require 'netsuite_rails/list_sync'
11
- require 'netsuite_rails/list_sync/pull_manager'
12
- require 'netsuite_rails/url_helper'
18
+ require 'netsuite_rails/list_sync/poll_manager'
13
19
 
14
20
  module NetSuiteRails
15
21
 
22
+ def self.rails4?
23
+ Rails::VERSION::MAJOR >= 4
24
+ end
25
+
16
26
  class Railtie < ::Rails::Railtie
17
27
  rake_tasks do
18
28
  load 'netsuite_rails/tasks/netsuite.rb'
@@ -4,8 +4,13 @@ module NetSuiteRails
4
4
 
5
5
  validates :key, presence: true, uniqueness: true
6
6
 
7
+ def self.for_class(klass)
8
+ self.where(key: "netsuite_poll_#{klass.to_s.downcase}timestamp").first_or_initialize
9
+ end
10
+
7
11
  def self.table_name_prefix
8
12
  'netsuite_'
9
13
  end
14
+
10
15
  end
11
16
  end
@@ -1,5 +1,5 @@
1
1
  module NetSuiteRails
2
- class PollManager
2
+ class PollTrigger
3
3
 
4
4
  class << self
5
5
 
@@ -33,7 +33,7 @@ module NetSuiteRails
33
33
 
34
34
  Rails.logger.info "NetSuite: Syncing #{klass.to_s}"
35
35
 
36
- preference = PollTimestamp.where(key: "netsuite_poll_#{klass.to_s.downcase}timestamp").first_or_initialize
36
+ preference = PollTimestamp.for_class(klass)
37
37
 
38
38
  # check if we've never synced before
39
39
  if preference.new_record?
@@ -43,7 +43,7 @@ module NetSuiteRails
43
43
  last_poll_date = preference.value
44
44
  last_poll_date = DateTime.parse(last_poll_date) unless last_poll_date.is_a?(DateTime)
45
45
 
46
- if DateTime.now - last_poll_date > sync_frequency
46
+ if DateTime.now.to_i - last_poll_date.to_i > sync_frequency
47
47
  Rails.logger.info "NetSuite: Syncing #{klass} modified since #{last_poll_date}"
48
48
  klass.netsuite_poll({ last_poll: last_poll_date }.merge(opts))
49
49
  else
@@ -55,6 +55,21 @@ module NetSuiteRails
55
55
  preference.save!
56
56
  end
57
57
  end
58
+
59
+ def update_local_records(opts = {})
60
+ record_models = opts[:record_models] || @record_models
61
+ list_models = opts[:list_models] || @list_models
62
+
63
+ # TODO only records are supported right now
64
+ # list_models.each do |klass|
65
+ # Rails.logger.info "NetSuite: Syncing #{klass}"
66
+ # klass.netsuite_poll
67
+ # end
68
+
69
+ record_models.each do |klass|
70
+ NetSuiteRails::RecordSync::PollManager.update_local_records(klass, opts)
71
+ end
72
+ end
58
73
 
59
74
  end
60
75
 
@@ -0,0 +1,186 @@
1
+ module NetSuiteRails
2
+ module RecordSync
3
+ module PollManager
4
+ extend self
5
+
6
+ def update_local_records(klass, opts = {})
7
+ klass.select([:netsuite_id, :id]).find_in_batches(batch_size: NetSuiteRails::Configuration.polling_page_size) do |local_batch|
8
+ netsuite_batch = if klass.netsuite_custom_record?
9
+ NetSuite::Records::CustomRecord.get_list(
10
+ list: local_batch.map(&:netsuite_id),
11
+ type_id: klass.netsuite_custom_record_type_id,
12
+ allow_incomplete: true
13
+ )
14
+ else
15
+ klass.netsuite_record_class.get_list(
16
+ list: local_batch.map(&:netsuite_id),
17
+ allow_incomplete: true
18
+ )
19
+ end
20
+
21
+ unless netsuite_batch
22
+ binding.pry
23
+ end
24
+
25
+ netsuite_batch.each do |netsuite_record|
26
+ self.process_search_result_item(klass, opts, netsuite_record)
27
+ end
28
+ end
29
+ end
30
+
31
+ def poll(klass, opts = {})
32
+ opts = {
33
+ import_all: false,
34
+ page_size: NetSuiteRails::Configuration.polling_page_size,
35
+ }.merge(opts)
36
+
37
+ opts[:netsuite_record_class] ||= klass.netsuite_record_class
38
+ opts[:saved_search_id] ||= klass.netsuite_sync_options[:saved_search_id]
39
+
40
+ search = opts[:netsuite_record_class].search(
41
+ poll_criteria(klass, opts).merge({
42
+ preferences: {
43
+ body_fields_only: false,
44
+ page_size: opts[:page_size]
45
+ }
46
+ })
47
+ )
48
+
49
+ # TODO more robust error reporting
50
+ unless search
51
+ raise 'error running netsuite sync'
52
+ end
53
+
54
+ process_search_results(klass, opts, search)
55
+ end
56
+
57
+ def poll_criteria(klass, opts)
58
+ search_criteria = {
59
+ criteria: {
60
+ basic: poll_basic_criteria(klass, opts)
61
+ }
62
+ }
63
+
64
+ if opts[:saved_search_id]
65
+ search_criteria[:criteria][:saved] = opts[:saved_search_id]
66
+ end
67
+
68
+ if needs_get_list?(opts)
69
+ search_criteria[:columns] = {
70
+ 'listRel:basic' => [
71
+ 'platformCommon:internalId/' => {},
72
+ ],
73
+ }
74
+ end
75
+
76
+ search_criteria
77
+ end
78
+
79
+ def poll_basic_criteria(klass, opts)
80
+ opts = {
81
+ criteria: [],
82
+ # last_poll: DateTime
83
+ }.merge(opts)
84
+
85
+ # allow custom criteria to be passed directly to the sync call
86
+ criteria = opts[:criteria] || []
87
+
88
+ # allow custom criteria from the model level
89
+ criteria += klass.netsuite_sync_options[:criteria] || []
90
+
91
+ if opts[:netsuite_record_class] == NetSuite::Records::CustomRecord
92
+ opts[:netsuite_custom_record_type_id] ||= klass.netsuite_custom_record_type_id
93
+
94
+ criteria << {
95
+ field: 'recType',
96
+ operator: 'is',
97
+ value: NetSuite::Records::CustomRecordRef.new(internal_id: opts[:netsuite_custom_record_type_id])
98
+ }
99
+ end
100
+
101
+ unless opts[:import_all]
102
+ criteria << {
103
+ # CustomRecordSearchBasic uses lastModified instead of the standard lastModifiedDate
104
+ field: (klass.netsuite_custom_record?) ? 'lastModified' : 'lastModifiedDate',
105
+ operator: 'after',
106
+ value: opts[:last_poll]
107
+ }
108
+ end
109
+
110
+ criteria
111
+ end
112
+
113
+ def process_search_results(klass, opts, search)
114
+ opts = {
115
+ skip_existing: false,
116
+ full_record_data: -1,
117
+ }.merge(opts)
118
+
119
+ Rails.logger.info "NetSuite: Processing #{search.total_records} over #{search.total_pages} pages"
120
+
121
+ # TODO need to improve the conditional here to match the get_list call conditional belo
122
+ if opts[:import_all] && opts[:skip_existing]
123
+ synced_netsuite_list = klass.pluck(:netsuite_id)
124
+ end
125
+
126
+ search.results_in_batches do |batch|
127
+ # a saved search is processed as a advanced search; advanced search often does not allow you to retrieve
128
+ # all of the fields (ex: addressbooklist on customer) that a normal search does
129
+ # the only way to get those fields is to pull down the full record again using getAll
130
+
131
+ if needs_get_list?(opts)
132
+ filtered_netsuite_id_list = batch.map(&:internal_id).map(&:to_i)
133
+
134
+ if opts[:skip_existing] == true
135
+ filtered_netsuite_id_list.reject! { |netsuite_id| synced_netsuite_list.include?(netsuite_id) }
136
+ end
137
+
138
+ if filtered_netsuite_id_list.present?
139
+ opts[:netsuite_record_class].get_list(list: filtered_netsuite_id_list)
140
+ else
141
+ []
142
+ end
143
+ else
144
+ batch
145
+ end.each do |netsuite_record|
146
+ self.process_search_result_item(klass, opts, netsuite_record)
147
+ end
148
+ end
149
+ end
150
+
151
+ def process_search_result_item(klass, opts, netsuite_record)
152
+ local_record = klass.where(netsuite_id: netsuite_record.internal_id).first_or_initialize
153
+
154
+ # when importing lots of records during an import_all skipping imported records is important
155
+ return if opts[:skip_existing] == true && !local_record.new_record?
156
+
157
+ local_record.netsuite_extract_from_record(netsuite_record)
158
+
159
+ # TODO optionally throw fatal errors; we want to skip fatal errors on intial import
160
+
161
+ unless local_record.save
162
+ Rails.logger.error "NetSuite: Error pulling record #{klass} NS ID #{netsuite_record.internal_id} #{local_record.errors.full_messages}"
163
+ end
164
+ end
165
+
166
+ def needs_get_list?(opts)
167
+ (opts[:saved_search_id].present? && opts[:full_record_data] != false) || opts[:full_record_data] == true
168
+ end
169
+
170
+ # TODO this should remain in the pull manager
171
+
172
+ def extract_custom_field_value(custom_field_value)
173
+ if custom_field_value.present? && custom_field_value.is_a?(Hash) && custom_field_value.has_key?(:name)
174
+ custom_field_value = custom_field_value[:name]
175
+ end
176
+
177
+ if custom_field_value.present? && custom_field_value.is_a?(NetSuite::Records::CustomRecordRef)
178
+ custom_field_value = custom_field_value.attributes[:name]
179
+ end
180
+
181
+ custom_field_value
182
+ end
183
+
184
+ end
185
+ end
186
+ end
@@ -1,147 +1,20 @@
1
1
  module NetSuiteRails
2
2
  module RecordSync
3
+ module PullManager
4
+ extend self
3
5
 
4
- class PullManager
5
- class << self
6
-
7
- def poll(klass, opts = {})
8
- opts = {
9
- import_all: false,
10
- page_size: NetSuiteRails::Configuration.polling_page_size,
11
- }.merge(opts)
12
-
13
- opts[:netsuite_record_class] ||= klass.netsuite_record_class
14
-
15
- search = opts[:netsuite_record_class].search(
16
- poll_criteria(klass, opts).merge({
17
- preferences: {
18
- body_fields_only: false,
19
- page_size: opts[:page_size]
20
- }
21
- })
22
- )
23
-
24
- # TODO more robust error reporting
25
- unless search
26
- raise 'error running netsuite sync'
27
- end
28
-
29
- process_search_results(klass, opts, search)
6
+ def extract_custom_field_value(custom_field_value)
7
+ if custom_field_value.present? && custom_field_value.is_a?(Hash) && custom_field_value.has_key?(:name)
8
+ custom_field_value = custom_field_value[:name]
30
9
  end
31
10
 
32
- def poll_criteria(klass, opts)
33
- search_criteria = {
34
- criteria: {
35
- basic: poll_basic_criteria(klass, opts)
36
- }
37
- }
38
-
39
- saved_search_id = opts[:saved_search_id] || klass.netsuite_sync_options[:saved_search_id]
40
-
41
- if saved_search_id
42
- search_criteria[:criteria][:saved] = saved_search_id
43
- end
44
-
45
- if needs_get_list?(opts)
46
- search_criteria[:columns] = {
47
- 'listRel:basic' => [
48
- 'platformCommon:internalId/' => {},
49
- ],
50
- }
51
- end
52
-
53
- search_criteria
54
- end
55
-
56
- def poll_basic_criteria(klass, opts)
57
- opts = {
58
- criteria: [],
59
- # last_poll: DateTime
60
- }.merge(opts)
61
-
62
- # allow custom criteria to be passed directly to the sync call
63
- criteria = opts[:criteria] || []
64
-
65
- # allow custom criteria from the model level
66
- criteria += klass.netsuite_sync_options[:criteria] || []
67
-
68
- if opts[:netsuite_record_class] == NetSuite::Records::CustomRecord
69
- opts[:netsuite_custom_record_type_id] ||= klass.netsuite_custom_record_type_id
70
-
71
- criteria << {
72
- field: 'recType',
73
- operator: 'is',
74
- value: NetSuite::Records::CustomRecordRef.new(internal_id: opts[:netsuite_custom_record_type_id])
75
- }
76
- end
77
-
78
- unless opts[:import_all]
79
- criteria << {
80
- # CustomRecordSearchBasic uses lastModified instead of the standard lastModifiedDate
81
- field: (klass.netsuite_custom_record?) ? 'lastModified' : 'lastModifiedDate',
82
- operator: 'after',
83
- value: opts[:last_poll]
84
- }
85
- end
86
-
87
- criteria
88
- end
89
-
90
- def process_search_results(klass, opts, search)
91
- opts = {
92
- skip_existing: false,
93
- full_record_data: -1,
94
- }.merge(opts)
95
-
96
- Rails.logger.info "NetSuite: Processing #{search.total_records} over #{search.total_pages} pages"
97
-
98
- # TODO need to improve the conditional here to match the get_list call conditional belo
99
- if opts[:import_all] && opts[:skip_existing]
100
- synced_netsuite_list = klass.pluck(:netsuite_id)
101
- end
102
-
103
- search.results_in_batches do |batch|
104
- # a saved search is processed as a advanced search; advanced search often does not allow you to retrieve
105
- # all of the fields (ex: addressbooklist on customer) that a normal search does
106
- # the only way to get those fields is to pull down the full record again using getAll
107
-
108
- if needs_get_list?(opts)
109
- filtered_netsuite_id_list = batch.map(&:internal_id)
110
-
111
- if opts[:skip_existing] == true
112
- filtered_netsuite_id_list.reject! { |netsuite_id| synced_netsuite_list.include?(netsuite_id) }
113
- end
114
-
115
- opts[:netsuite_record_class].get_list(list: batch.map(&:internal_id))
116
- else
117
- batch
118
- end.each do |netsuite_record|
119
- self.process_search_result_item(klass, opts, netsuite_record)
120
- end
121
- end
122
- end
123
-
124
- def process_search_result_item(klass, opts, netsuite_record)
125
- local_record = klass.where(netsuite_id: netsuite_record.internal_id).first_or_initialize
126
-
127
- # when importing lots of records during an import_all skipping imported records is important
128
- return if opts[:skip_existing] == true && !local_record.new_record?
129
-
130
- local_record.netsuite_extract_from_record(netsuite_record)
131
-
132
- # TODO optionally throw fatal errors; we want to skip fatal errors on intial import
133
-
134
- unless local_record.save
135
- Rails.logger.error "NetSuite: Error pulling record #{klass} NS ID #{netsuite_record.internal_id} #{local_record.errors.full_messages}"
136
- end
137
- end
138
-
139
- def needs_get_list?(opts)
140
- (opts[:saved_search_id].present? && opts[:full_record_data] != false) || opts[:full_record_data] == true
11
+ if custom_field_value.present? && custom_field_value.is_a?(NetSuite::Records::CustomRecordRef)
12
+ custom_field_value = custom_field_value.attributes[:name]
141
13
  end
142
14
 
15
+ custom_field_value
143
16
  end
144
- end
145
17
 
18
+ end
146
19
  end
147
- end
20
+ end