module NetSuite::Utilities

Public Instance Methods

append_memo(ns_record, added_memo, opts = {}) click to toggle source
# File lib/netsuite/utilities.rb, line 15
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 = {}) { || ... } click to toggle source
# File lib/netsuite/utilities.rb, line 72
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 << 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!() click to toggle source

TODO need structured logger for various statements

# File lib/netsuite/utilities.rb, line 9
def clear_cache!
  @netsuite_get_record_cache = {}
  @netsuite_find_record_cache = {}
  DataCenter.clear_cache!
end
data_center_url(*args) click to toggle source

TODO consider what to dop with this duplicate data center implementation

# File lib/netsuite/utilities.rb, line 68
def data_center_url(*args)
  DataCenter.get(*args)
end
find_record(record, names, opts = {}) click to toggle source
# File lib/netsuite/utilities.rb, line 223
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|
    @netsuite_find_record_cache ||= {}

    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) click to toggle source
# File lib/netsuite/utilities.rb, line 155
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 = {}) click to toggle source
# File lib/netsuite/utilities.rb, line 164
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::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::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 = {}) click to toggle source
# File lib/netsuite/utilities.rb, line 187
def get_record(record_klass, id, opts = {})
  opts[:external_id] ||= false

  if opts[:cache]
    @netsuite_get_record_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) click to toggle source
# File lib/netsuite/utilities.rb, line 42
def netsuite_data_center_urls(account_id)
  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: {}
  }).call(:get_data_center_urls, message: {
    'platformMsgs:account' => account_id
  })

  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_server_time() click to toggle source
# File lib/netsuite/utilities.rb, line 37
def netsuite_server_time
  server_time_response = NetSuite::Utilities.backoff { NetSuite::Configuration.connection.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) click to toggle source

assumes UTC0 unix timestamp

# File lib/netsuite/utilities.rb, line 272
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) click to toggle source
# File lib/netsuite/utilities.rb, line 150
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