netsuite 0.8.2 → 0.8.7

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 (143) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +1 -0
  3. data/.ruby-version +1 -1
  4. data/.tool-versions +1 -0
  5. data/Gemfile +4 -3
  6. data/README.md +129 -38
  7. data/circle.yml +36 -13
  8. data/lib/netsuite.rb +40 -19
  9. data/lib/netsuite/actions/login.rb +20 -1
  10. data/lib/netsuite/actions/search.rb +1 -6
  11. data/lib/netsuite/actions/update.rb +6 -2
  12. data/lib/netsuite/actions/update_list.rb +109 -0
  13. data/lib/netsuite/actions/upsert.rb +2 -0
  14. data/lib/netsuite/configuration.rb +34 -4
  15. data/lib/netsuite/errors.rb +1 -0
  16. data/lib/netsuite/records/accounting_period.rb +2 -2
  17. data/lib/netsuite/records/assembly_build.rb +4 -1
  18. data/lib/netsuite/records/assembly_item.rb +1 -0
  19. data/lib/netsuite/records/assembly_unbuild.rb +3 -0
  20. data/lib/netsuite/records/bin_number.rb +18 -0
  21. data/lib/netsuite/records/bin_number_list.rb +1 -20
  22. data/lib/netsuite/records/bin_transfer.rb +38 -0
  23. data/lib/netsuite/records/bin_transfer_inventory.rb +20 -0
  24. data/lib/netsuite/records/bin_transfer_inventory_list.rb +10 -0
  25. data/lib/netsuite/records/cash_refund_item.rb +1 -1
  26. data/lib/netsuite/records/classification.rb +5 -2
  27. data/lib/netsuite/records/contact.rb +1 -1
  28. data/lib/netsuite/records/credit_memo.rb +1 -1
  29. data/lib/netsuite/records/custom_field_list.rb +10 -2
  30. data/lib/netsuite/records/custom_record.rb +3 -3
  31. data/lib/netsuite/records/custom_record_ref.rb +1 -0
  32. data/lib/netsuite/records/customer.rb +5 -4
  33. data/lib/netsuite/records/customer_credit_cards.rb +36 -0
  34. data/lib/netsuite/records/customer_credit_cards_list.rb +10 -0
  35. data/lib/netsuite/records/customer_deposit.rb +9 -6
  36. data/lib/netsuite/records/customer_payment.rb +6 -2
  37. data/lib/netsuite/records/customer_payment_credit.rb +17 -0
  38. data/lib/netsuite/records/customer_payment_credit_list.rb +12 -0
  39. data/lib/netsuite/records/customer_sales_team.rb +24 -0
  40. data/lib/netsuite/records/customer_sales_team_list.rb +9 -0
  41. data/lib/netsuite/records/customer_status.rb +29 -0
  42. data/lib/netsuite/records/customer_subscription.rb +18 -0
  43. data/lib/netsuite/records/customer_subscriptions_list.rb +10 -0
  44. data/lib/netsuite/records/employee.rb +1 -1
  45. data/lib/netsuite/records/entity_custom_field.rb +53 -0
  46. data/lib/netsuite/records/estimate.rb +42 -0
  47. data/lib/netsuite/records/estimate_item.rb +40 -0
  48. data/lib/netsuite/records/estimate_item_list.rb +11 -0
  49. data/lib/netsuite/records/inbound_shipment.rb +33 -0
  50. data/lib/netsuite/records/inbound_shipment_item.rb +39 -0
  51. data/lib/netsuite/records/inbound_shipment_item_list.rb +11 -0
  52. data/lib/netsuite/records/inter_company_journal_entry.rb +48 -0
  53. data/lib/netsuite/records/inter_company_journal_entry_line.rb +28 -0
  54. data/lib/netsuite/records/inter_company_journal_entry_line_list.rb +14 -0
  55. data/lib/netsuite/records/inventory_item.rb +3 -2
  56. data/lib/netsuite/records/invoice.rb +1 -1
  57. data/lib/netsuite/records/item_fulfillment.rb +1 -1
  58. data/lib/netsuite/records/lot_numbered_inventory_item.rb +116 -0
  59. data/lib/netsuite/records/matrix_option_list.rb +12 -4
  60. data/lib/netsuite/records/message.rb +30 -0
  61. data/lib/netsuite/records/non_inventory_resale_item.rb +3 -2
  62. data/lib/netsuite/records/non_inventory_sale_item.rb +1 -1
  63. data/lib/netsuite/records/other_charge_sale_item.rb +2 -2
  64. data/lib/netsuite/records/partner.rb +7 -5
  65. data/lib/netsuite/records/price.rb +17 -0
  66. data/lib/netsuite/records/price_level.rb +26 -0
  67. data/lib/netsuite/records/price_list.rb +9 -0
  68. data/lib/netsuite/records/pricing.rb +20 -0
  69. data/lib/netsuite/records/pricing_matrix.rb +2 -2
  70. data/lib/netsuite/records/promotions.rb +26 -0
  71. data/lib/netsuite/records/promotions_list.rb +9 -0
  72. data/lib/netsuite/records/return_authorization_item.rb +1 -1
  73. data/lib/netsuite/records/sales_order.rb +1 -0
  74. data/lib/netsuite/records/sales_order_item.rb +12 -5
  75. data/lib/netsuite/records/sales_role.rb +26 -0
  76. data/lib/netsuite/records/sales_tax_item.rb +3 -1
  77. data/lib/netsuite/records/serialized_assembly_item.rb +239 -0
  78. data/lib/netsuite/records/service_resale_item.rb +1 -1
  79. data/lib/netsuite/records/service_sale_item.rb +1 -1
  80. data/lib/netsuite/records/support_case.rb +1 -1
  81. data/lib/netsuite/records/support_case_type.rb +26 -0
  82. data/lib/netsuite/records/tax_group.rb +2 -2
  83. data/lib/netsuite/records/transaction_body_custom_field.rb +61 -0
  84. data/lib/netsuite/records/transaction_column_custom_field.rb +59 -0
  85. data/lib/netsuite/records/vendor.rb +2 -1
  86. data/lib/netsuite/records/vendor_credit.rb +2 -0
  87. data/lib/netsuite/records/vendor_currency.rb +26 -0
  88. data/lib/netsuite/records/vendor_currency_list.rb +9 -0
  89. data/lib/netsuite/records/work_order.rb +8 -0
  90. data/lib/netsuite/support/actions.rb +2 -0
  91. data/lib/netsuite/support/country.rb +27 -15
  92. data/lib/netsuite/support/search_result.rb +20 -5
  93. data/lib/netsuite/utilities.rb +83 -21
  94. data/lib/netsuite/version.rb +1 -1
  95. data/netsuite.gemspec +4 -3
  96. data/spec/netsuite/actions/login_spec.rb +23 -0
  97. data/spec/netsuite/actions/update_list_spec.rb +107 -0
  98. data/spec/netsuite/actions/update_spec.rb +42 -0
  99. data/spec/netsuite/configuration_spec.rb +111 -6
  100. data/spec/netsuite/records/address_spec.rb +10 -0
  101. data/spec/netsuite/records/basic_record_spec.rb +19 -2
  102. data/spec/netsuite/records/bin_number_spec.rb +23 -0
  103. data/spec/netsuite/records/classification_spec.rb +10 -1
  104. data/spec/netsuite/records/custom_field_list_spec.rb +39 -4
  105. data/spec/netsuite/records/custom_record_spec.rb +1 -1
  106. data/spec/netsuite/records/customer_credit_cards_list_spec.rb +23 -0
  107. data/spec/netsuite/records/customer_payment_credit_list_spec.rb +26 -0
  108. data/spec/netsuite/records/customer_payment_spec.rb +1 -6
  109. data/spec/netsuite/records/customer_sales_team_list_spec.rb +41 -0
  110. data/spec/netsuite/records/customer_spec.rb +44 -2
  111. data/spec/netsuite/records/customer_subscription_spec.rb +41 -0
  112. data/spec/netsuite/records/customer_subscriptions_list_spec.rb +19 -0
  113. data/spec/netsuite/records/employee_spec.rb +2 -2
  114. data/spec/netsuite/records/entity_custom_field_spec.rb +34 -0
  115. data/spec/netsuite/records/estimate_item_list_spec.rb +26 -0
  116. data/spec/netsuite/records/estimate_item_spec.rb +40 -0
  117. data/spec/netsuite/records/estimate_spec.rb +216 -0
  118. data/spec/netsuite/records/inter_company_journal_entry_line_list_spec.rb +26 -0
  119. data/spec/netsuite/records/inter_company_journal_entry_line_spec.rb +60 -0
  120. data/spec/netsuite/records/inter_company_journal_entry_spec.rb +156 -0
  121. data/spec/netsuite/records/inventory_item_spec.rb +57 -0
  122. data/spec/netsuite/records/matrix_option_list_spec.rb +15 -5
  123. data/spec/netsuite/records/message_spec.rb +49 -0
  124. data/spec/netsuite/records/non_inventory_resale_item_spec.rb +165 -0
  125. data/spec/netsuite/records/non_inventory_sale_item_spec.rb +1 -1
  126. data/spec/netsuite/records/partner_spec.rb +143 -0
  127. data/spec/netsuite/records/price_level_spec.rb +16 -0
  128. data/spec/netsuite/records/pricing_matrix_spec.rb +15 -13
  129. data/spec/netsuite/records/return_authorization_item_spec.rb +1 -1
  130. data/spec/netsuite/records/sales_order_item_spec.rb +11 -5
  131. data/spec/netsuite/records/service_resale_item_spec.rb +134 -0
  132. data/spec/netsuite/records/support_case_type_spec.rb +22 -0
  133. data/spec/netsuite/records/transaction_body_custom_field_spec.rb +32 -0
  134. data/spec/netsuite/records/transaction_column_custom_field_spec.rb +32 -0
  135. data/spec/netsuite/records/vendor_credit_spec.rb +29 -0
  136. data/spec/netsuite/records/vendor_spec.rb +1 -1
  137. data/spec/netsuite/support/search_result_spec.rb +24 -0
  138. data/spec/netsuite/utilities_spec.rb +44 -6
  139. data/spec/spec_helper.rb +5 -4
  140. data/spec/support/fixtures/update_list/update_list_items.xml +22 -0
  141. data/spec/support/fixtures/update_list/update_list_one_item.xml +18 -0
  142. data/spec/support/fixtures/update_list/update_list_with_errors.xml +32 -0
  143. metadata +111 -11
@@ -17,9 +17,10 @@ module NetSuite
17
17
  # <platformCore:pageIndex>1</platformCore:pageIndex>
18
18
  # <platformCore:searchId>WEBSERVICES_738944_SB2_03012013650784545962753432_28d96bd280</platformCore:searchId>
19
19
 
20
- def initialize(response, result_class)
20
+ def initialize(response, result_class, credentials)
21
21
  @result_class = result_class
22
22
  @response = response
23
+ @credentials = credentials
23
24
 
24
25
  @total_records = response.body[:total_records].to_i
25
26
  @total_pages = response.body[:total_pages].to_i
@@ -28,8 +29,19 @@ module NetSuite
28
29
  if @total_records > 0
29
30
  if response.body.has_key?(:record_list)
30
31
  # basic search results
31
- record_list = response.body[:record_list][:record]
32
- record_list = [record_list] unless record_list.is_a?(Array)
32
+
33
+ # `recordList` node can contain several nested `record` nodes, only one node or be empty
34
+ # so we have to handle all these cases:
35
+ # * { record_list: nil }
36
+ # * { record_list: { record: => {...} } }
37
+ # * { record_list: { record: => [{...}, {...}, ...] } }
38
+ record_list = if response.body[:record_list].nil?
39
+ []
40
+ elsif response.body[:record_list][:record].is_a?(Array)
41
+ response.body[:record_list][:record]
42
+ else
43
+ [response.body[:record_list][:record]]
44
+ end
33
45
 
34
46
  record_list.each do |record|
35
47
  results << result_class.new(record)
@@ -98,8 +110,11 @@ module NetSuite
98
110
  yield results
99
111
 
100
112
  next_search = @result_class.search(
101
- search_id: @response.body[:search_id],
102
- page_index: @response.body[:page_index].to_i + 1
113
+ {
114
+ search_id: @response.body[:search_id],
115
+ page_index: @response.body[:page_index].to_i + 1
116
+ },
117
+ @credentials
103
118
  )
104
119
 
105
120
  @results = next_search.results
@@ -1,3 +1,5 @@
1
+ require 'date'
2
+
1
3
  module NetSuite
2
4
  module Utilities
3
5
  extend self
@@ -37,6 +39,36 @@ module NetSuite
37
39
  server_time_response.body[:get_server_time_response][:get_server_time_result][:server_time]
38
40
  end
39
41
 
42
+ def netsuite_data_center_urls(account_id)
43
+ data_center_call_response = NetSuite::Configuration.connection({
44
+ # NOTE force a production WSDL so the sandbox settings are ignored
45
+ # as of 1/20/18 NS will start using the account ID to determine
46
+ # if a account is sandbox (123_SB1) as opposed to using a sandbox domain
47
+
48
+ wsdl: 'https://webservices.netsuite.com/wsdl/v2017_2_0/netsuite.wsdl',
49
+
50
+ # NOTE don't inherit default namespace settings, it includes the API version
51
+ namespaces: {
52
+ 'xmlns:platformCore' => "urn:core_2017_2.platform.webservices.netsuite.com"
53
+ },
54
+
55
+ soap_header: {}
56
+ }).call(:get_data_center_urls, message: {
57
+ 'platformMsgs:account' => account_id
58
+ })
59
+
60
+ if data_center_call_response.success?
61
+ data_center_call_response.body[:get_data_center_urls_response][:get_data_center_urls_result][:data_center_urls]
62
+ else
63
+ false
64
+ end
65
+ end
66
+
67
+ # TODO consider what to dop with this duplicate data center implementation
68
+ def data_center_url(*args)
69
+ DataCenter.get(*args)
70
+ end
71
+
40
72
  def backoff(options = {})
41
73
  # TODO the default backoff attempts should be customizable the global config
42
74
  options[:attempts] ||= 8
@@ -46,7 +78,7 @@ module NetSuite
46
78
  begin
47
79
  count += 1
48
80
  yield
49
- rescue Exception => e
81
+ rescue StandardError => e
50
82
  exceptions_to_retry = [
51
83
  Errno::ECONNRESET,
52
84
  Errno::ETIMEDOUT,
@@ -83,12 +115,20 @@ module NetSuite
83
115
  # https://github.com/stripe/stripe-netsuite/issues/815
84
116
  if !e.message.include?("Only one request may be made against a session at a time") &&
85
117
  !e.message.include?('java.util.ConcurrentModificationException') &&
118
+ !e.message.include?('java.lang.NullPointerException') &&
119
+ !e.message.include?('java.lang.IllegalStateException') &&
120
+ !e.message.include?('java.lang.reflect.InvocationTargetException') &&
86
121
  !e.message.include?('com.netledger.common.exceptions.NLDatabaseOfflineException') &&
87
122
  !e.message.include?('com.netledger.database.NLConnectionUtil$NoCompanyDbsOnlineException') &&
88
123
  !e.message.include?('com.netledger.cache.CacheUnavailableException') &&
124
+ !e.message.include?('java.lang.IllegalStateException') &&
89
125
  !e.message.include?('An unexpected error occurred.') &&
126
+ !e.message.include?('An unexpected error has occurred. Technical Support has been alerted to this problem.') &&
90
127
  !e.message.include?('Session invalidation is in progress with different thread') &&
128
+ !e.message.include?('[missing resource APP:ERRORMESSAGE:WS_AN_UNEXPECTED_ERROR_OCCURRED] [missing resource APP:ERRORMESSAGE:ERROR_ID_1]') &&
91
129
  !e.message.include?('SuiteTalk concurrent request limit exceeded. Request blocked.') &&
130
+ # maintenance is the new outage: this message is being used for intermittent errors
131
+ !e.message.include?('The account you are trying to access is currently unavailable while we undergo our regularly scheduled maintenance.') &&
92
132
  !e.message.include?('The Connection Pool is not intialized.') &&
93
133
  # it looks like NetSuite mispelled their error message...
94
134
  !e.message.include?('The Connection Pool is not intiialized.')
@@ -109,15 +149,16 @@ module NetSuite
109
149
 
110
150
  def request_failed?(ns_object)
111
151
  return false if ns_object.errors.nil? || ns_object.errors.empty?
152
+ ns_object.errors.any? { |x| x.type == "ERROR" }
153
+ end
112
154
 
113
- warnings = ns_object.errors.select { |x| x.type == "WARN" }
114
- errors = ns_object.errors.select { |x| x.type == "ERROR" }
115
-
116
- # warnings.each do |warn|
117
- # log.warn(warn.message, code: warn.code)
118
- # end
155
+ def get_field_options(recordType, fieldName)
156
+ options = NetSuite::Records::BaseRefList.get_select_value(
157
+ field: fieldName,
158
+ recordType: recordType
159
+ )
119
160
 
120
- return errors.size > 0
161
+ options.base_refs
121
162
  end
122
163
 
123
164
  def get_item(ns_item_internal_id, opts = {})
@@ -133,6 +174,7 @@ module NetSuite
133
174
  ns_item ||= NetSuite::Utilities.get_record(NetSuite::Records::KitItem, ns_item_internal_id, opts)
134
175
  ns_item ||= NetSuite::Utilities.get_record(NetSuite::Records::SerializedInventoryItem, ns_item_internal_id, opts)
135
176
  ns_item ||= NetSuite::Utilities.get_record(NetSuite::Records::LotNumberedAssemblyItem, ns_item_internal_id, opts)
177
+ ns_item ||= NetSuite::Utilities.get_record(NetSuite::Records::LotNumberedInventoryItem, ns_item_internal_id, opts)
136
178
 
137
179
  if ns_item.nil?
138
180
  fail NetSuite::RecordNotFound, "item with ID #{ns_item_internal_id} not found"
@@ -196,7 +238,11 @@ module NetSuite
196
238
  field_name = 'email'
197
239
  end
198
240
 
199
- field_name ||= 'name'
241
+ field_name ||= if record.to_s.end_with?('Item')
242
+ 'displayName'
243
+ else
244
+ 'name'
245
+ end
200
246
 
201
247
  # TODO remove backoff when it's built-in to search
202
248
  search = backoff { record.search({
@@ -217,25 +263,41 @@ module NetSuite
217
263
  nil
218
264
  end
219
265
 
220
- def data_center_url(*args)
221
- DataCenter.get(*args)
222
- end
223
-
224
266
  # http://mikebian.co/notes-on-dates-timezones-with-netsuites-suitetalk-api/
267
+ # https://wyeworks.com/blog/2016/6/22/behavior-changes-in-ruby-2.4
268
+ # https://github.com/rails/rails/commit/c9c5788a527b70d7f983e2b4b47e3afd863d9f48
269
+
225
270
  # assumes UTC0 unix timestamp
226
271
  def normalize_time_to_netsuite_date(unix_timestamp)
227
272
  # convert to date to eliminate hr/min/sec
228
- time = Time.at(unix_timestamp).utc.to_date.to_datetime
229
-
230
- offset = 8
231
- time = time.new_offset("-08:00")
232
-
233
- if time.to_time.dst?
234
- offset = 7
273
+ time = Time.at(unix_timestamp).
274
+ utc.
275
+ to_date.
276
+ to_datetime
277
+
278
+ # tzinfo allows us to determine the dst status of the time being passed in
279
+ # NetSuite requires that the time be passed to the API with the PDT TZ offset
280
+ # of the time passed in (i.e. not the current TZ offset of PDT)
281
+
282
+ if defined?(TZInfo)
283
+ # if no version is defined, less than 2.0
284
+ # https://github.com/tzinfo/tzinfo/blob/master/CHANGES.md#added
285
+ if !defined?(TZInfo::VERSION)
286
+ # https://stackoverflow.com/questions/2927111/ruby-get-time-in-given-timezone
287
+ offset = TZInfo::Timezone.get("America/Los_Angeles").period_for_utc(time).utc_total_offset_rational
288
+ time = time.new_offset(offset)
289
+ else
290
+ time = TZInfo::Timezone.get("America/Los_Angeles").utc_to_local(time)
291
+ offset = time.offset
292
+ end
293
+ else
294
+ # if tzinfo is not installed, let's give it our best guess: -7
295
+ offset = Rational(-7, 24)
235
296
  time = time.new_offset("-07:00")
236
297
  end
237
298
 
238
- (time + Rational(offset, 24)).iso8601
299
+ time = (time + (offset * -1))
300
+ time.iso8601
239
301
  end
240
302
 
241
303
  end
@@ -1,3 +1,3 @@
1
1
  module NetSuite
2
- VERSION = '0.8.2'
2
+ VERSION = '0.8.7'
3
3
  end
data/netsuite.gemspec CHANGED
@@ -2,8 +2,9 @@
2
2
  require File.expand_path('../lib/netsuite/version', __FILE__)
3
3
 
4
4
  Gem::Specification.new do |gem|
5
+ gem.licenses = ['MIT']
5
6
  gem.authors = ['Ryan Moran', 'Michael Bianco']
6
- gem.email = ['ryan.moran@gmail.com', 'mike@cliffsidemedia.com']
7
+ gem.email = ['ryan.moran@gmail.com', 'mike@mikebian.co']
7
8
  gem.description = %q{NetSuite SuiteTalk API Wrapper}
8
9
  gem.summary = %q{NetSuite SuiteTalk API (SOAP) Wrapper}
9
10
  gem.homepage = 'https://github.com/NetSweet/netsuite'
@@ -15,7 +16,7 @@ Gem::Specification.new do |gem|
15
16
  gem.require_paths = ['lib']
16
17
  gem.version = NetSuite::VERSION
17
18
 
18
- gem.add_dependency 'savon', '>= 2.3.0'
19
+ gem.add_dependency 'savon', '>= 2.3.0', '<= 2.11.1'
19
20
 
20
- gem.add_development_dependency 'rspec', '~> 3.1.0'
21
+ gem.add_development_dependency 'rspec', '~> 3.8.0'
21
22
  end
@@ -41,4 +41,27 @@ describe NetSuite::Actions::Login do
41
41
  role: 234
42
42
  }) }.to raise_error(Savon::SOAPFault)
43
43
  end
44
+
45
+ it 'handles a login call when token based auth is in place' do
46
+ NetSuite.configure do
47
+ consumer_key '123'
48
+ consumer_secret '123'
49
+ token_id '123'
50
+ token_secret '123'
51
+
52
+ api_version '2017_2'
53
+ end
54
+
55
+ message = {"platformMsgs:passport"=>{"platformCore:email"=>"email", "platformCore:password"=>"password", "platformCore:account"=>"1234", "platformCore:role"=>234}}
56
+ savon.expects(:login).with(:message => message).returns(File.read('spec/support/fixtures/login/success.xml'))
57
+
58
+ result = NetSuite::Actions::Login.call({
59
+ email: 'email',
60
+ password: 'password',
61
+ role: 234
62
+ })
63
+
64
+ expect(result.success?).to eq(true)
65
+ expect(result.body[:user_id]).to_not be_nil
66
+ end
44
67
  end
@@ -0,0 +1,107 @@
1
+ require 'spec_helper'
2
+
3
+ describe NetSuite::Actions::UpdateList do
4
+ before { savon.mock! }
5
+ after { savon.unmock! }
6
+
7
+ context 'Items' do
8
+ context 'one item' do
9
+ let(:item) do
10
+ [
11
+ NetSuite::Records::InventoryItem.new(internal_id: '624113', item_id: 'Target', upccode: 'Target')
12
+ ]
13
+ end
14
+
15
+ before do
16
+ savon.expects(:update_list).with(:message =>
17
+ {
18
+ 'record' => [{
19
+ 'listAcct:itemId' => 'Target',
20
+ '@xsi:type' => 'listAcct:InventoryItem',
21
+ '@internalId' => '624113'
22
+ }]
23
+ }).returns(File.read('spec/support/fixtures/update_list/update_list_one_item.xml'))
24
+ end
25
+
26
+ it 'makes a valid request to the NetSuite API' do
27
+ NetSuite::Actions::UpdateList.call(item)
28
+ end
29
+
30
+ it 'returns a valid Response object' do
31
+ response = NetSuite::Actions::UpdateList.call(item)
32
+ expect(response).to be_kind_of(NetSuite::Response)
33
+ expect(response).to be_success
34
+ end
35
+ end
36
+
37
+ context 'two items' do
38
+ let(:items) do
39
+ [
40
+ NetSuite::Records::InventoryItem.new(internal_id: '624172', item_id: 'Shutter Fly', upccode: 'Shutter Fly, Inc.'),
41
+ NetSuite::Records::InventoryItem.new(internal_id: '624113', item_id: 'Target', upccode: 'Target')
42
+ ]
43
+ end
44
+
45
+ before do
46
+ savon.expects(:update_list).with(:message =>
47
+ {
48
+ 'record' => [{
49
+ 'listAcct:itemId' => 'Shutter Fly',
50
+ '@xsi:type' => 'listAcct:InventoryItem',
51
+ '@internalId' => '624172'
52
+ },
53
+ {
54
+ 'listAcct:itemId' => 'Target',
55
+ '@xsi:type' => 'listAcct:InventoryItem',
56
+ '@internalId' => '624113'
57
+ }
58
+ ]
59
+ }).returns(File.read('spec/support/fixtures/update_list/update_list_items.xml'))
60
+ end
61
+
62
+ it 'makes a valid request to the NetSuite API' do
63
+ NetSuite::Actions::UpdateList.call(items)
64
+ end
65
+
66
+ it 'returns a valid Response object' do
67
+ response = NetSuite::Actions::UpdateList.call(items)
68
+ expect(response).to be_kind_of(NetSuite::Response)
69
+ expect(response).to be_success
70
+ end
71
+ end
72
+ end
73
+
74
+ context 'with errors' do
75
+ let(:items) do
76
+ [
77
+ NetSuite::Records::InventoryItem.new(internal_id: '624172-bad', item_id: 'Shutter Fly', upccode: 'Shutter Fly, Inc.'),
78
+ NetSuite::Records::InventoryItem.new(internal_id: '624113-bad', item_id: 'Target', upccode: 'Target')
79
+ ]
80
+ end
81
+
82
+ before do
83
+ savon.expects(:update_list).with(:message =>
84
+ {
85
+ 'record' => [{
86
+ 'listAcct:itemId' => 'Shutter Fly',
87
+ '@xsi:type' => 'listAcct:InventoryItem',
88
+ '@internalId' => '624172-bad'
89
+ },
90
+ {
91
+ 'listAcct:itemId' => 'Target',
92
+ '@xsi:type' => 'listAcct:InventoryItem',
93
+ '@internalId' => '624113-bad'
94
+ }
95
+ ]
96
+ }).returns(File.read('spec/support/fixtures/update_list/update_list_with_errors.xml'))
97
+ end
98
+
99
+ it 'constructs error objects' do
100
+ response = NetSuite::Actions::UpdateList.call(items)
101
+ expect(response.errors.keys).to match_array(['624172', '624113'])
102
+ expect(response.errors['624172'].first.code).to eq('USER_ERROR')
103
+ expect(response.errors['624172'].first.message).to eq('Please enter value(s) for: ItemId')
104
+ expect(response.errors['624172'].first.type).to eq('ERROR')
105
+ end
106
+ end
107
+ end
@@ -19,6 +19,48 @@ describe NetSuite::Actions::Update do
19
19
  }
20
20
  end
21
21
 
22
+ describe 'updating the external ID' do
23
+ let(:response) { NetSuite::Response.new(:success => true, :body => { :internal_id => '1' }) }
24
+
25
+ # https://github.com/NetSweet/netsuite/pull/416
26
+ # if the external ID is set, and the external ID field is ommitted on an update, the external ID field does not change
27
+ # if the external_id field is set to nil, it should not be passed to netsuite
28
+ # passing an empty string to the external ID field does not remove it
29
+
30
+ it 'does not pass the external ID to an update call if not modified or included in update options' do
31
+ expect(NetSuite::Actions::Update).to receive(:call).
32
+ with([customer.class, {}], {}).
33
+ and_return(response)
34
+
35
+ expect(customer.update).to be_truthy
36
+ end
37
+
38
+ it 'should update the external ID when the attribute on the record is set' do
39
+ expect(NetSuite::Actions::Update).to receive(:call).
40
+ with([customer.class, {external_id: 'foo'}], {}).
41
+ and_return(response)
42
+
43
+ customer.external_id = 'foo'
44
+ expect(customer.update).to be_truthy
45
+ end
46
+
47
+ it 'should update the external ID to nil when the attribute on the record is set' do
48
+ expect(NetSuite::Actions::Update).to receive(:call).
49
+ with([customer.class, {external_id: nil}], {}).
50
+ and_return(response)
51
+
52
+ expect(customer.update(external_id: nil)).to be_truthy
53
+ end
54
+
55
+ it 'should update the external ID to the options value not the attribute value' do
56
+ expect(NetSuite::Actions::Update).to receive(:call).
57
+ with([customer.class, {external_id: 'bar'}], {}).
58
+ and_return(response)
59
+
60
+ customer.external_id = 'foo'
61
+ expect(customer.update(external_id: 'bar')).to be_truthy
62
+ end
63
+ end
22
64
 
23
65
  context 'when successful' do
24
66
 
@@ -29,10 +29,12 @@ describe NetSuite::Configuration do
29
29
  end
30
30
 
31
31
  describe '#connection' do
32
+ EXAMPLE_ENDPOINT = 'https://1023.suitetalk.api.netsuite.com/services/NetSuitePort_2020_2'
32
33
  before(:each) do
33
34
  # reset clears out the password info
34
35
  config.email 'me@example.com'
35
36
  config.password 'me@example.com'
37
+ config.endpoint EXAMPLE_ENDPOINT
36
38
  config.account 1023
37
39
  config.wsdl "my_wsdl"
38
40
  config.api_version "2012_2"
@@ -57,6 +59,19 @@ describe NetSuite::Configuration do
57
59
 
58
60
  expect(config).to have_received(:cached_wsdl)
59
61
  end
62
+
63
+ it 'sets the endpoint on the Savon client' do
64
+ # this is ugly/brittle, but it's hard to see how else to test this
65
+ savon_configs = config.connection.globals.instance_eval {@options}
66
+ expect(savon_configs.fetch(:endpoint)).to eq(EXAMPLE_ENDPOINT)
67
+ end
68
+
69
+ it 'handles a nil endpoint' do
70
+ config.endpoint = nil
71
+ # this is ugly/brittle, but it's hard to see how else to test this
72
+ savon_configs = config.connection.globals.instance_eval {@options}
73
+ expect(savon_configs.fetch(:endpoint)).to eq(nil)
74
+ end
60
75
  end
61
76
 
62
77
  describe '#wsdl' do
@@ -166,6 +181,23 @@ describe NetSuite::Configuration do
166
181
  end
167
182
  end
168
183
 
184
+ describe '#endpoint' do
185
+ it 'can be set with endpoint=' do
186
+ config.endpoint = 42
187
+ expect(config.endpoint).to eq(42)
188
+ end
189
+
190
+ it 'can be set with just endpoint(value)' do
191
+ config.endpoint(42)
192
+ expect(config.endpoint).to eq(42)
193
+ end
194
+
195
+ it 'supports nil endpoints' do
196
+ config.endpoint = nil
197
+ expect(config.endpoint).to eq(nil)
198
+ end
199
+ end
200
+
169
201
  describe '#auth_header' do
170
202
  context 'when doing user authentication' do
171
203
  before do
@@ -326,15 +358,11 @@ describe NetSuite::Configuration do
326
358
 
327
359
  describe "#credentials" do
328
360
  context "when none are defined" do
329
- skip "should properly create the auth credentials" do
330
-
331
- end
361
+ skip "should properly create the auth credentials"
332
362
  end
333
363
 
334
364
  context "when they are defined" do
335
- it "should properly replace the default auth credentials" do
336
-
337
- end
365
+ skip "should properly replace the default auth credentials"
338
366
  end
339
367
  end
340
368
 
@@ -371,4 +399,81 @@ describe NetSuite::Configuration do
371
399
  end
372
400
  end
373
401
 
402
+ describe "#log" do
403
+ it 'allows a file path to be set as the log destination' do
404
+ file_path = Tempfile.new.path
405
+ config.log = file_path
406
+ config.logger.info "foo"
407
+
408
+ log_contents = open(file_path).read
409
+ expect(log_contents).to include("foo")
410
+ end
411
+
412
+ it 'allows an IO device to bet set as the log destination' do
413
+ stream = StringIO.new
414
+ config.log = stream
415
+ config.logger.info "foo"
416
+
417
+ expect(stream.string).to include("foo")
418
+ end
419
+ end
420
+
421
+ describe '#log_level' do
422
+ it 'defaults to :debug' do
423
+ expect(config.log_level).to eq(:debug)
424
+ end
425
+
426
+ it 'can be initially set to any log level' do
427
+ config.log_level(:info)
428
+
429
+ expect(config.log_level).to eq(:info)
430
+ end
431
+
432
+ it 'can override itself' do
433
+ config.log_level = :info
434
+
435
+ expect(config.log_level).to eq(:info)
436
+
437
+ config.log_level(:debug)
438
+
439
+ expect(config.log_level).to eq(:debug)
440
+ end
441
+ end
442
+
443
+ describe '#log_level=' do
444
+ it 'can set the initial log_level' do
445
+ config.log_level = :info
446
+
447
+ expect(config.log_level).to eq(:info)
448
+ end
449
+
450
+ it 'can override a previously set log level' do
451
+ config.log_level = :info
452
+
453
+ expect(config.log_level).to eq(:info)
454
+
455
+ config.log_level = :debug
456
+
457
+ expect(config.log_level).to eq(:debug)
458
+ end
459
+ end
460
+
461
+ describe 'timeouts' do
462
+ it 'has defaults' do
463
+ expect(config.read_timeout).to eql(60)
464
+ expect(config.open_timeout).to be_nil
465
+ end
466
+
467
+ it 'sets timeouts' do
468
+ config.read_timeout = 100
469
+ config.open_timeout = 60
470
+
471
+ expect(config.read_timeout).to eql(100)
472
+ expect(config.open_timeout).to eql(60)
473
+
474
+ # ensure no exception is raised
475
+ config.connection
476
+ end
477
+ end
478
+
374
479
  end