Module: NetSuite::Utilities

Extended by:
Utilities
Included in:
Utilities
Defined in:
lib/netsuite/utilities.rb,
lib/netsuite/utilities/strings.rb,
lib/netsuite/utilities/data_center.rb

Defined Under Namespace

Modules: Strings Classes: DataCenter

Instance Method Summary collapse

Instance Method Details

#append_memo(ns_record, added_memo, opts = {}) ⇒ Object



37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
# File 'lib/netsuite/utilities.rb', line 37

def append_memo(ns_record, added_memo, opts = {})
  opts[:skip_if_exists] ||= false

  memo_key = if ns_record.class == NetSuite::Records::Customer
    :comments
  else
    :memo
  end

  return if opts[:skip_if_exists] &&
    ns_record.send(memo_key) &&
    ns_record.send(memo_key).include?(added_memo)

  if ns_record.send(memo_key)
    ns_record.send(:"#{memo_key}=", "#{ns_record.send(memo_key)}. #{added_memo}")
  else
    ns_record.send(:"#{memo_key}=", added_memo.to_s)
  end

  ns_record
end

#backoff(options = {}) ⇒ Object



94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
# File 'lib/netsuite/utilities.rb', line 94

def backoff(options = {})
  # TODO the default backoff attempts should be customizable the global config
  options[:attempts] ||= 8

  count = 0

  begin
    count += 1
    yield
  rescue StandardError => e
    exceptions_to_retry = [
      Errno::ECONNRESET,
      Errno::ETIMEDOUT,
      Errno::EHOSTUNREACH,
      EOFError,
      Wasabi::Resolver::HTTPError,
      Savon::SOAPFault,
      Savon::InvalidResponseError,
      Zlib::BufError,
      Savon::HTTPError,
      SocketError,
      Net::OpenTimeout
    ]

    # available in ruby > 1.9
    if defined?(Net::ReadTimeout)
      exceptions_to_retry << Net::ReadTimeout
    end

    # available in ruby > 2.2.0
    exceptions_to_retry << IO::EINPROGRESSWaitWritable if defined?(IO::EINPROGRESSWaitWritable)
    exceptions_to_retry << OpenSSL::SSL::SSLErrorWaitReadable if defined?(OpenSSL::SSL::SSLErrorWaitReadable)

    # depends on the http library chosen
    exceptions_to_retry << HTTPI::SSLError if defined?(HTTPI::SSLError)
    exceptions_to_retry << HTTPI::TimeoutError if defined?(HTTPI::TimeoutError)
    exceptions_to_retry << HTTPClient::TimeoutError if defined?(HTTPClient::TimeoutError)
    exceptions_to_retry << HTTPClient::ConnectTimeoutError if defined?(HTTPClient::ConnectTimeoutError)
    exceptions_to_retry << HTTPClient::ReceiveTimeoutError if defined?(HTTPClient::ReceiveTimeoutError)
    exceptions_to_retry << HTTPClient::SendTimeoutError if defined?(HTTPClient::SendTimeoutError)
    exceptions_to_retry << Excon::Error::Timeout if defined?(Excon::Error::Timeout)
    exceptions_to_retry << Excon::Error::Socket if defined?(Excon::Error::Socket)

    if !exceptions_to_retry.include?(e.class)
      raise
    end

    # whitelist certain SOAPFaults; all other network errors should automatically retry
    if e.is_a?(Savon::SOAPFault)
      # https://github.com/stripe/stripe-netsuite/issues/815
      if !e.message.include?("Only one request may be made against a session at a time") &&
        !e.message.include?('java.util.ConcurrentModificationException') &&
        !e.message.include?('java.lang.NullPointerException') &&
        !e.message.include?('java.lang.IllegalStateException') &&
        !e.message.include?('java.lang.reflect.InvocationTargetException') &&
        !e.message.include?('com.netledger.common.exceptions.NLDatabaseOfflineException') &&
        !e.message.include?('com.netledger.database.NLConnectionUtil$NoCompanyDbsOnlineException') &&
        !e.message.include?('com.netledger.cache.CacheUnavailableException') &&
        !e.message.include?('java.lang.IllegalStateException') &&
        !e.message.include?('An unexpected error occurred.') &&
        !e.message.include?('An unexpected error has occurred.  Technical Support has been alerted to this problem.') &&
        !e.message.include?('Session invalidation is in progress with different thread') &&
        !e.message.include?('[missing resource APP:ERRORMESSAGE:WS_AN_UNEXPECTED_ERROR_OCCURRED] [missing resource APP:ERRORMESSAGE:ERROR_ID_1]') &&
        !e.message.include?('SuiteTalk concurrent request limit exceeded. Request blocked.') &&
        # maintenance is the new outage: this message is being used for intermittent errors
        !e.message.include?('The account you are trying to access is currently unavailable while we undergo our regularly scheduled maintenance.') &&
        !e.message.include?('The Connection Pool is not intialized.') &&
        # it looks like NetSuite mispelled their error message...
        !e.message.include?('The Connection Pool is not intiialized.')
        raise
      end
    end

    if count >= options[:attempts]
      raise
    end

    # log.warn("concurrent request failure", sleep: count, attempt: count)
    sleep(count)

    retry
  end
end

#clear_cache!Object

TODO need structured logger for various statements



9
10
11
12
13
14
15
16
17
18
19
# File 'lib/netsuite/utilities.rb', line 9

def clear_cache!
  if NetSuite::Configuration.multi_tenant?
    Thread.current[:netsuite_gem_netsuite_get_record_cache] = {}
    Thread.current[:netsuite_gem_netsuite_find_record_cache] = {}
  else
    @netsuite_get_record_cache = {}
    @netsuite_find_record_cache = {}
  end

  DataCenter.clear_cache!
end

#data_center_url(*args) ⇒ Object

TODO consider what to dop with this duplicate data center implementation



90
91
92
# File 'lib/netsuite/utilities.rb', line 90

def data_center_url(*args)
  DataCenter.get(*args)
end

#find_record(record, names, opts = {}) ⇒ Object



252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
# File 'lib/netsuite/utilities.rb', line 252

def find_record(record, names, opts = {})
  field_name = opts[:field_name]

  names = [ names ] if names.is_a?(String)

  # FIXME: Records that have the same name but different types will break
  # the cache
  names.each do |name|
    if netsuite_find_record_cache.has_key?(name)
      return netsuite_find_record_cache[name]
    end

    # sniff for an email-like input; useful for employee/customer searches
    if !field_name && /@.*\./ =~ name
      field_name = 'email'
    end

    field_name ||= if record.to_s.end_with?('Item')
      'displayName'
    else
      'name'
    end

    # TODO remove backoff when it's built-in to search
    search = backoff { record.search({
      basic: [
        {
          field: field_name,
          operator: 'contains',
          value: name,
        }
      ]
    }) }

    if search.results.first
      return netsuite_find_record_cache[name] = search.results.first
    end
  end

  nil
end

#get_field_options(recordType, fieldName) ⇒ Object



183
184
185
186
187
188
189
190
# File 'lib/netsuite/utilities.rb', line 183

def get_field_options(recordType, fieldName)
  options = NetSuite::Records::BaseRefList.get_select_value(
    field: fieldName,
    recordType: recordType
  )

  options.base_refs
end

#get_item(ns_item_internal_id, opts = {}) ⇒ Object



192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
# File 'lib/netsuite/utilities.rb', line 192

def get_item(ns_item_internal_id, opts = {})
  # TODO add additional item types!
  ns_item = NetSuite::Utilities.get_record(NetSuite::Records::InventoryItem, ns_item_internal_id, opts)
  ns_item ||= NetSuite::Utilities.get_record(NetSuite::Records::AssemblyItem, ns_item_internal_id, opts)
  ns_item ||= NetSuite::Utilities.get_record(NetSuite::Records::NonInventorySaleItem, ns_item_internal_id, opts)
  ns_item ||= NetSuite::Utilities.get_record(NetSuite::Records::NonInventoryResaleItem, ns_item_internal_id, opts)
  ns_item ||= NetSuite::Utilities.get_record(NetSuite::Records::DiscountItem, ns_item_internal_id, opts)
  ns_item ||= NetSuite::Utilities.get_record(NetSuite::Records::OtherChargeSaleItem, ns_item_internal_id, opts)
  ns_item ||= NetSuite::Utilities.get_record(NetSuite::Records::ServiceSaleItem, ns_item_internal_id, opts)
  ns_item ||= NetSuite::Utilities.get_record(NetSuite::Records::ServiceResaleItem, ns_item_internal_id, opts)
  ns_item ||= NetSuite::Utilities.get_record(NetSuite::Records::GiftCertificateItem, ns_item_internal_id, opts)
  ns_item ||= NetSuite::Utilities.get_record(NetSuite::Records::KitItem, ns_item_internal_id, opts)
  ns_item ||= NetSuite::Utilities.get_record(NetSuite::Records::ItemGroup, ns_item_internal_id, opts)
  ns_item ||= NetSuite::Utilities.get_record(NetSuite::Records::SerializedInventoryItem, ns_item_internal_id, opts)
  ns_item ||= NetSuite::Utilities.get_record(NetSuite::Records::SerializedAssemblyItem, ns_item_internal_id, opts)
  ns_item ||= NetSuite::Utilities.get_record(NetSuite::Records::LotNumberedAssemblyItem, ns_item_internal_id, opts)
  ns_item ||= NetSuite::Utilities.get_record(NetSuite::Records::LotNumberedInventoryItem, ns_item_internal_id, opts)

  if ns_item.nil?
    fail NetSuite::RecordNotFound, "item with ID #{ns_item_internal_id} not found"
  end

  ns_item
end

#get_record(record_klass, id, opts = {}) ⇒ Object



217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
# File 'lib/netsuite/utilities.rb', line 217

def get_record(record_klass, id, opts = {})
  opts[:external_id] ||= false

  if opts[:cache]
    netsuite_get_record_cache[record_klass.to_s] ||= {}

    if netsuite_get_record_cache[record_klass.to_s].has_key?(id.to_i)
      return netsuite_get_record_cache[record_klass.to_s][id.to_i]
    end
  end

  begin
    # log.debug("get record", netsuite_record_type: record_klass.name, netsuite_record_id: id)

    ns_record = if opts[:external_id]
      backoff { record_klass.get(external_id: id) }
    else
      backoff { record_klass.get(id) }
    end

    if opts[:cache]
      netsuite_get_record_cache[record_klass.to_s][id.to_i] = ns_record
    end

    return ns_record
  rescue ::NetSuite::RecordNotFound
    # log.warn("record not found", ns_record_type: record_klass.name, ns_record_id: id)
    if opts[:cache]
      netsuite_get_record_cache[record_klass.to_s][id.to_i] = nil
    end

    return nil
  end
end

#netsuite_data_center_urls(account_id, credentials = {}) ⇒ Object



64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
# File 'lib/netsuite/utilities.rb', line 64

def netsuite_data_center_urls(, credentials={})
  data_center_call_response = NetSuite::Configuration.connection({
    # NOTE force a production WSDL so the sandbox settings are ignored
    #      as of 1/20/18 NS will start using the account ID to determine
    #      if a account is sandbox (123_SB1) as opposed to using a sandbox domain

    wsdl: 'https://webservices.netsuite.com/wsdl/v2017_2_0/netsuite.wsdl',

    # NOTE don't inherit default namespace settings, it includes the API version
    namespaces: {
      'xmlns:platformCore' => "urn:core_2017_2.platform.webservices.netsuite.com"
    },

    soap_header: {}
  }, credentials).call(:get_data_center_urls, message: {
    'platformMsgs:account' => 
  })

  if data_center_call_response.success?
    data_center_call_response.body[:get_data_center_urls_response][:get_data_center_urls_result][:data_center_urls]
  else
    false
  end
end

#netsuite_find_record_cacheObject



29
30
31
32
33
34
35
# File 'lib/netsuite/utilities.rb', line 29

def netsuite_find_record_cache
  if NetSuite::Configuration.multi_tenant?
    Thread.current[:netsuite_gem_netsuite_find_record_cache] ||= {}
  else
    @netsuite_find_record_cache ||= {}
  end
end

#netsuite_get_record_cacheObject



21
22
23
24
25
26
27
# File 'lib/netsuite/utilities.rb', line 21

def netsuite_get_record_cache
  if NetSuite::Configuration.multi_tenant?
    Thread.current[:netsuite_gem_netsuite_get_record_cache] ||= {}
  else
    @netsuite_get_record_cache ||= {}
  end
end

#netsuite_server_time(credentials = {}) ⇒ Object



59
60
61
62
# File 'lib/netsuite/utilities.rb', line 59

def netsuite_server_time(credentials={})
  server_time_response = NetSuite::Utilities.backoff { NetSuite::Configuration.connection({}, credentials).call(:get_server_time) }
  server_time_response.body[:get_server_time_response][:get_server_time_result][:server_time]
end

#normalize_time_to_netsuite_date(unix_timestamp) ⇒ Object

assumes UTC0 unix timestamp



299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
# File 'lib/netsuite/utilities.rb', line 299

def normalize_time_to_netsuite_date(unix_timestamp)
  # convert to date to eliminate hr/min/sec
  time = Time.at(unix_timestamp).
    utc.
    to_date.
    to_datetime

  # tzinfo allows us to determine the dst status of the time being passed in
  # NetSuite requires that the time be passed to the API with the PDT TZ offset
  # of the time passed in (i.e. not the current TZ offset of PDT)

  if defined?(TZInfo)
    # if no version is defined, less than 2.0
    # https://github.com/tzinfo/tzinfo/blob/master/CHANGES.md#added
    if !defined?(TZInfo::VERSION)
      # https://stackoverflow.com/questions/2927111/ruby-get-time-in-given-timezone
      offset = TZInfo::Timezone.get("America/Los_Angeles").period_for_utc(time).utc_total_offset_rational
      time = time.new_offset(offset)
    else
      time = TZInfo::Timezone.get("America/Los_Angeles").utc_to_local(time)
      offset = time.offset
    end
  else
    # if tzinfo is not installed, let's give it our best guess: -7
    offset = Rational(-7, 24)
    time = time.new_offset("-07:00")
  end

  time = (time + (offset * -1))
  time.iso8601
end

#request_failed?(ns_object) ⇒ Boolean

Returns:

  • (Boolean)


178
179
180
181
# File 'lib/netsuite/utilities.rb', line 178

def request_failed?(ns_object)
  return false if ns_object.errors.nil? || ns_object.errors.empty?
  ns_object.errors.any? { |x| x.type == "ERROR" }
end