class ReactiveShipping::BTVP

Constants

REACTIVE_FREIGHT_CARRIER

Public Instance Methods

find_bol(tracking_number, options = {}) click to toggle source

Documents

# File lib/reactive_freight/carriers/btvp.rb, line 16
def find_bol(tracking_number, options = {})
  options = @options.merge(options)
  parse_document_response(:bol, tracking_number, options)
end
find_pod(tracking_number, options = {}) click to toggle source
# File lib/reactive_freight/carriers/btvp.rb, line 21
def find_pod(tracking_number, options = {})
  options = @options.merge(options)
  parse_document_response(:pod, tracking_number, options)
end
find_rates(origin, destination, packages, options = {}) click to toggle source

Rates

# File lib/reactive_freight/carriers/btvp.rb, line 27
def find_rates(origin, destination, packages, options = {})
  options = @options.merge(options)
  origin = Location.from(origin)
  destination = Location.from(destination)
  packages = Array(packages)

  request = build_rate_request(origin, destination, packages, options)
  parse_rate_response(origin, destination, packages, commit(:rates, request))
end
find_tracking_info(tracking_number, options = {}) click to toggle source

Tracking

# File lib/reactive_freight/carriers/btvp.rb, line 38
def find_tracking_info(tracking_number, options = {})
  options = @options.merge(options)
  request = build_tracking_request(tracking_number)
  parse_tracking_response(commit(:track, request))
end
requirements() click to toggle source
# File lib/reactive_freight/carriers/btvp.rb, line 11
def requirements
  %i[username password]
end

Protected Instance Methods

build_rate_request(origin, destination, packages, options = {}) click to toggle source

Rates

# File lib/reactive_freight/carriers/btvp.rb, line 145
def build_rate_request(origin, destination, packages, options = {})
  options = @options.merge(options)

  accessorials = []

  unless options[:accessorials].blank?
    serviceable_accessorials?(options[:accessorials])
    options[:accessorials].each do |a|
      unless @conf.dig(:accessorials, :unserviceable).include?(a)
        accessorials << { code: @conf.dig(:accessorials, :mappable)[a] }
      end
    end
  end

  accessorials = accessorials.uniq.to_a

  request = {
    'arg0' => {
      securityinfo: build_soap_header,
      quote: {
        iam: options[:iam].blank? ? 'D' : options[:iam], # S for shipper, C for consignee, D for third party
        shipper: {
          city: origin.to_hash[:city].to_s.upcase,
          state: origin.to_hash[:province].to_s.upcase,
          zip: origin.to_hash[:postal_code].to_s.upcase
        },
        consignee: {
          city: destination.to_hash[:city].to_s.upcase,
          state: destination.to_hash[:province].to_s.upcase,
          zip: destination.to_hash[:postal_code].to_s.upcase
        },
        accessorialcount: accessorials.size,
        accessorial: accessorials.blank? ? [] : accessorials,
        ppdcol: options[:payment_type].blank? ? 'P' : options[:payment_type].blank?, # Prepaid
        itemcount: packages.size,
        item: packages.inject([]) do |arr, package|
          arr << {
            _class: package.freight_class,
            description: 'Freight'.upcase, # Required
            haz: '', # Y if yes
            pallets: 1,
            pieces: 1,
            weight: package.pounds.ceil
          }
        end
      }
    }
  }

  save_request(request)
  request
end
build_soap_header() click to toggle source
# File lib/reactive_freight/carriers/btvp.rb, line 46
def build_soap_header
  {
    username: @options[:username],
    password: @options[:password]
  }
end
build_tracking_request(tracking_number) click to toggle source

Tracking

# File lib/reactive_freight/carriers/btvp.rb, line 279
def build_tracking_request(tracking_number)
  request = {
    'arg0' => {
      securityinfo: build_soap_header,
      pronumber: tracking_number
    }
  }

  save_request(request)
  request
end
build_url(action) click to toggle source
# File lib/reactive_freight/carriers/btvp.rb, line 80
def build_url(action)
  scheme = @conf.dig(:api, :use_ssl, action) ? 'https://' : 'http://'
  "#{scheme}#{@conf.dig(:api, :domain)}:#{@conf.dig(:api, :ports, action)}#{@conf.dig(:api, :endpoints, action)}"
end
commit(action, request) click to toggle source
# File lib/reactive_freight/carriers/btvp.rb, line 53
def commit(action, request)
  Savon.client(
    wsdl: build_url(action),
    convert_request_keys_to: :upcase,
    env_namespace: :soapenv
  ).call(
    @conf.dig(:api, :actions, action),
    headers: { 'SOAPAction' => '""' },
    soap_action: false,
    message: request
  ).body
end
download_document(type, tracking_number, url, options = {}) click to toggle source

Documents

# File lib/reactive_freight/carriers/btvp.rb, line 90
def download_document(type, tracking_number, url, options = {})
  options = @options.merge(options)
  path = options[:path].blank? ? File.join(Dir.tmpdir, "#{@@name} #{tracking_number} #{type.to_s.upcase}.pdf") : options[:path]
  file = File.new(path, 'w')

  File.open(file.path, 'wb') do |file|
    URI.parse(url).open do |input|
      file.write(input.read)
    end
  rescue OpenURI::HTTPError
    raise ReactiveShipping::ResponseError, "API Error: #{@@name}: Document not found"
  end

  File.exist?(path) ? path : false
end
parse_date(date) click to toggle source
# File lib/reactive_freight/carriers/btvp.rb, line 66
def parse_date(date)
  date ? Date.strptime(date, '%m/%d/%Y').to_s(:db) : nil
end
parse_datetime(datetime) click to toggle source
# File lib/reactive_freight/carriers/btvp.rb, line 70
def parse_datetime(datetime)
  return nil unless datetime

  if datetime.include?('-')
    DateTime.strptime(datetime, '%Y-%m-%d %H:%M').to_s(:db)
  else
    DateTime.strptime(datetime, '%m/%d/%Y %H:%M').to_s(:db)
  end
end
parse_document_response(type, tracking_number, options = {}) click to toggle source
# File lib/reactive_freight/carriers/btvp.rb, line 106
def parse_document_response(type, tracking_number, options = {})
  options = @options.merge(options)
  browser = Watir::Browser.new(:chrome, headless: !@debug)
  browser.goto(build_url(:pod))

  browser.text_field(name: 'userid').set(@options[:username])
  browser.text_field(name: 'password').set(@options[:password])
  browser.button(name: 'btnLogin').click

  browser.element(xpath: '/html/body/div[1]/div[2]/div[2]/div[1]/div[2]/img').click
  browser.element(xpath: '/html/body/div[1]/div[2]/div[2]/div[1]/div[3]/ul[2]/li').click
  browser.element(xpath: '/html/body/div[1]/div[2]/div[2]/div[1]/div[3]/ul[2]/li[6]/span[2]').click
  browser.select_list(name: 'TATIWT').select('S')

  browser.textarea(name: 'TATFB').set(tracking_number)
  browser.button(xpath: '/html/body/div[1]/div[3]/div[3]/div/div[1]/div/button[2]').click

  browser.element(xpath: '/html/body/div[1]/div[3]/div[2]/div/div[1]/div[4]/div[3]/div/table/tbody/tr[2]/td[2]').double_click

  html = browser.element(xpath: '/html/body/div[1]/div[3]/div[2]/div/div/div/form/div[4]/div[2]/div/div/table').inner_html
  html = Nokogiri::HTML.parse(html)

  url = nil
  html.css('tr').each do |tr|
    next unless tr.text.downcase.include?(@conf.dig(:documents, :types, type).downcase)

    link_id = tr.css('td')[1].css('a').to_html.split('id=')[1].split('onfocus')[0].gsub('"', '').strip
    browser.element(css: "##{link_id}").click
    url = browser.element(xpath: '/html/body/div[1]/div[3]/div[2]/div/embed').attribute_value('src')
  end

  browser.close

  raise ReactiveShipping::ResponseError, "API Error: #{@@name}: Document not found" if url.blank?

  download_document(type, tracking_number, url, options)
end
parse_location(code) click to toggle source
# File lib/reactive_freight/carriers/btvp.rb, line 291
def parse_location(code)
  case code
  when 'LA'
    Location.new(
      city: 'Los Angeles',
      province: 'CA',
      state: 'CA',
      country: ActiveUtils::Country.find('USA')
    )
  else
    Location.new(
      city: code,
      province: nil,
      state: nil,
      country: ActiveUtils::Country.find('USA')
    )
  end
end
parse_rate_response(origin, destination, packages, response) click to toggle source
# File lib/reactive_freight/carriers/btvp.rb, line 198
def parse_rate_response(origin, destination, packages, response)
  success = true
  message = ''

  if !response
    success = false
    message = 'API Error: Unknown response'
  else
    if !response.dig(:getquote_response, :return, :rating, :errorcode).blank?
      success = false
      message = response.dig(:getquote_response, :return, :rating, :errorcode)
    else
      cost = (response.dig(:getquote_response, :return, :rating, :amount).to_f * 100).to_i

      longest_dimension = packages.inject([]) { |_arr, p| [p.length(:in), p.width(:in)] }.max.ceil
      if !@options[:tariff].blank?
        if longest_dimension >= 168
          cost += @options[:tariff].dig('overlength_fees').dig('over_14_ft')
        elsif longest_dimension >= 144 && longest_dimension < 168
          cost += @options[:tariff].dig('overlength_fees').dig('12_through_14_ft')
        elsif longest_dimension >= 120 && longest_dimension < 144
          cost += @options[:tariff].dig('overlength_fees').dig('10_through_12_ft')
        elsif longest_dimension >= 96 && longest_dimension < 120
          cost += @options[:tariff].dig('overlength_fees').dig('8_through_10_ft')
        end
      elsif longest_dimension >= 96
        warn 'API Warning: Overlength fees not applied because `tariff` is empty!'
      end

      transit_days = response.dig(
        :getquote_response,
        :return,
        :service,
        :days
      ).to_i

      # Calculate real transit time based on information we have about the destination service days
      %i[mon tue wed thu fri].each do |weekday|
        days += 1 if response.dig(:getquote_response, :return, :service, :destination, weekday) == 'N'
      end

      estimate_reference = response.dig(
        :getquote_response,
        :return,
        :rating,
        :quotenumber
      )

      if cost
        rate_estimates = [
          RateEstimate.new(
            origin,
            destination,
            { scac: self.class.scac.upcase, name: self.class.name },
            :standard,
            transit_days: transit_days,
            estimate_reference: estimate_reference,
            total_cost: cost,
            total_price: cost,
            currency: 'USD',
            with_excessive_length_fees: @conf.dig(:attributes, :rates, :with_excessive_length_fees)
          )
        ]
      else
        success = false
        message = 'API Error: Cost is emtpy'
      end
    end
  end

  RateResponse.new(
    success,
    message,
    response,
    rates: rate_estimates,
    response: response,
    request: last_request
  )
end
parse_tracking_response(response) click to toggle source
# File lib/reactive_freight/carriers/btvp.rb, line 310
def parse_tracking_response(response)
  unless response.dig(:tracktrace_response, :return, :currentstatus, :errorcode).blank?
    status = response.dig(:tracktrace_response, :return, :currentstatus, :errorcode)
    return TrackingResponse.new(false, status, response, carrier: "#{@@scac}, #{@@name}", xml: response, response: response, request: last_request)
  end

  receiver_address = Location.new(
    city: response.dig(:tracktrace_response, :return, :currentstatus, :consignee, :city).titleize,
    province: response.dig(:tracktrace_response, :return, :currentstatus, :consignee, :state).upcase,
    state: response.dig(:tracktrace_response, :return, :currentstatus, :consignee, :state).upcase,
    country: ActiveUtils::Country.find('USA')
  )

  shipper_address = Location.new(
    city: response.dig(:tracktrace_response, :return, :currentstatus, :shipper, :city).titleize,
    province: response.dig(:tracktrace_response, :return, :currentstatus, :shipper, :state).upcase,
    state: response.dig(:tracktrace_response, :return, :currentstatus, :shipper, :state).upcase,
    country: ActiveUtils::Country.find('USA')
  )

  actual_delivery_date = response.dig(:tracktrace_response, :return, :currentstatus, :deliverydate)
  unless actual_delivery_date.blank?
    comment = response.dig(:tracktrace_response, :return, :currentstatus, :status).downcase
    if comment.starts_with?('delivered')
      actual_delivery_date = parse_date(comment.downcase.split('signed')[0].split('on')[1].strip.sub('at ', ''))
    end
  end

  ship_time = parse_date(response.dig(:tracktrace_response, :return, :currentstatus, :shipdate))
  scheduled_delivery_date = parse_date(response.dig(:tracktrace_response, :return, :currentstatus, :estdeliverydate))
  tracking_number = response.dig(:tracktrace_response, :return, :pronumber)

  shipment_events = []
  response.dig(:tracktrace_response, :return, :history).each do |api_event|
    event = nil
    @conf.dig(:events, :types).each do |key, val|
      if api_event.dig(:description).downcase.include? val
        event = key
        break
      end
    end
    next if event.blank?

    datetime_without_time_zone = parse_datetime("#{api_event.dig(:date)} #{api_event.dig(:time)}")
    location = parse_location(api_event.dig(:location))

    # status and type_code set automatically by ActiveFreight based on event
    shipment_events << ShipmentEvent.new(event, datetime_without_time_zone, location)
  end

  shipment_events = shipment_events.sort_by(&:time)

  TrackingResponse.new(
    true,
    shipment_events.last.status,
    response,
    carrier: "#{@@scac}, #{@@name}",
    xml: response,
    response: response,
    status: status,
    type_code: shipment_events.last.status,
    ship_time: ship_time,
    scheduled_delivery_date: scheduled_delivery_date,
    actual_delivery_date: actual_delivery_date,
    delivery_signature: nil,
    shipment_events: shipment_events,
    shipper_address: shipper_address,
    origin: shipper_address,
    destination: receiver_address,
    tracking_number: tracking_number,
    request: last_request
  )
end
strip_date(str) click to toggle source
# File lib/reactive_freight/carriers/btvp.rb, line 85
def strip_date(str)
  str ? str.split(/[A|P]M /)[1] : nil
end