class Omniship::FedEx

:key is your developer API key :password is your API password :account is your FedEx account number :login is your meter number

Constants

CarrierCodes
DropoffTypes
LIVE_URL
PackageIdentifierTypes
PackageTypes
PaymentTypes
ServiceTypes
TEST_URL

Public Class Methods

service_name_for_code(service_code) click to toggle source
# File lib/omniship/carriers/fedex.rb, line 85
def self.service_name_for_code(service_code)
  ServiceTypes[service_code] || begin
    name = service_code.downcase.split('_').collect{|word| word.capitalize }.join(' ')
    "FedEx #{name.sub(/Fedex /, '')}"
  end
end

Public Instance Methods

create_shipment(origin, destination, packages, options={}) click to toggle source
# File lib/omniship/carriers/fedex.rb, line 105
def create_shipment(origin, destination, packages, options={})
  options        = @options.merge(options)
  options[:test] = options[:test].nil? ? true : options[:test]
  packages       = Array(packages)
  ship_request   = build_ship_request(origin, destination, packages, options)
  response       = commit(save_request(ship_request.gsub("\n", "")), options[:test])
  parse_ship_response(response, options)
end
delete_shipment(tracking_number, shipment_type, options={}) click to toggle source
# File lib/omniship/carriers/fedex.rb, line 114
def delete_shipment(tracking_number, shipment_type, options={})
  options                 = @options.merge(options)
  delete_shipment_request = build_delete_request(tracking_number, shipment_type, options)
  response                = commit(save_request(delete_shipment_request.gsub("\n", "")), options[:test])
  parse_delete_response(response, options)
end
find_rates(origin, destination, packages, options = {}) click to toggle source
# File lib/omniship/carriers/fedex.rb, line 96
def find_rates(origin, destination, packages, options = {})
  options        = @options.merge(options)
  options[:test] = options[:test].nil? ? true : options[:test]
  packages       = Array(packages)
  rate_request   = build_rate_request(origin, destination, packages, options)
  response       = commit(save_request(rate_request.gsub("\n", "")), options[:test])
  parse_rate_response(origin, destination, packages, response, options)
end
find_tracking_info(tracking_number, options={}) click to toggle source
# File lib/omniship/carriers/fedex.rb, line 121
def find_tracking_info(tracking_number, options={})
  options          = @options.update(options)
  tracking_request = build_tracking_request(tracking_number, options)
  response         = commit(save_request(tracking_request), (options[:test] || false)).gsub(/<(\/)?.*?\:(.*?)>/, '<\1\2>')
  parse_tracking_response(response, options)
end
requirements() click to toggle source
# File lib/omniship/carriers/fedex.rb, line 92
def requirements
  [:key, :account, :meter, :password]
end

Protected Instance Methods

build_access_request(xml) click to toggle source
# File lib/omniship/carriers/fedex.rb, line 338
def build_access_request(xml)
  xml.WebAuthenticationDetail {
    xml.UserCredential {
      xml.Key @options[:key]
      xml.Password @options[:password]
    }
  }

  xml.ClientDetail {
    xml.AccountNumber @options[:account]
    xml.MeterNumber @options[:meter]
  }

  xml.TransactionDetail {
    xml.CustomerTransactionId 'Omniship' # TODO: Need to do something better with this...
  }
end
build_delete_request(tracking_number, shipment_type, options={}) click to toggle source
# File lib/omniship/carriers/fedex.rb, line 293
def build_delete_request(tracking_number, shipment_type, options={})
  builder = Nokogiri::XML::Builder.new do |xml|
    xml.DeleteShipmentRequest('xmlns' => 'http://fedex.com/ws/ship/v12') {
      build_access_request(xml)
      xml.Version {
        xml.ServiceId "ship"
        xml.Major "12"
        xml.Intermediate "0"
        xml.Minor "0"
      }
      xml.ShipTimestamp options[:ship_timestamp] if options[:ship_timestamp]
      xml.TrackingId {
        xml.TrackingIdType shipment_type
        xml.TrackingNumber tracking_number
      }
      xml.DeletionControl options[:deletion_type] || "DELETE_ALL_PACKAGES"
    }
  end
  builder.to_xml
end
build_location_node(name, location, xml) click to toggle source
# File lib/omniship/carriers/fedex.rb, line 356
def build_location_node(name, location, xml)
  for name in name
    xml.send(name) {
      xml.Contact {
        xml.PersonName location.name unless location.name == "" || location.name == nil
        xml.CompanyName location.company unless location.company == "" || location.name == nil
        xml.PhoneNumber location.phone
      }
      xml.Address {
        xml.StreetLines location.address1
        xml.StreetLines location.address2 unless location.address2.nil?
        xml.City location.city
        xml.StateOrProvinceCode location.state
        xml.PostalCode location.postal_code
        xml.CountryCode location.country_code(:alpha2)
        xml.Residential true unless location.commercial?
      }
    }
  end
end
build_rate_request(origin, destination, packages, options={}) click to toggle source
# File lib/omniship/carriers/fedex.rb, line 129
def build_rate_request(origin, destination, packages, options={})
  imperial = ['US','LR','MM'].include?(origin.country_code(:alpha2))

  builder = Nokogiri::XML::Builder.new do |xml|
    xml.RateRequest('xmlns' => 'http://fedex.com/ws/rate/v12') {
      build_access_request(xml)
      xml.Version {
        xml.ServiceId "crs"
        xml.Major "12"
        xml.Intermediate "0"
        xml.Minor "0"
      }
      xml.ReturnTransitAndCommit options[:return_transit_and_commit] || false
      xml.VariableOptions "SATURDAY_DELIVERY"
      xml.RequestedShipment {
        xml.ShipTimestamp options[:ship_date] || DateTime.now.strftime
        xml.DropoffType options[:dropoff_type] || 'REGULAR_PICKUP'
        xml.PackagingType options[:packaging_type] || 'YOUR_PACKAGING'
        build_location_node(['Shipper'], (options[:shipper] || origin), xml)
        build_location_node(['Recipient'], destination, xml)
        if options[:shipper] && options[:shipper] != origin
          build_location_node(['Origin'], origin, xml)
        end
        xml.RateRequestTypes 'ACCOUNT'
        xml.PackageCount packages.size
        packages.each do |pkg|
          xml.RequestedPackageLineItems {
            xml.SequenceNumber 1
            xml.GroupPackageCount 1
            xml.Weight {
              xml.Units (imperial ? 'LB' : 'KG')
              xml.Value ((imperial ? pkg.weight : pkg.weight/2.2).to_f)
            }
            xml.SpecialServicesRequested {
              if options[:without_signature]
                xml.SpecialServiceTypes "SIGNATURE_OPTION"
                xml.SignatureOptionDetail {
                  xml.OptionType "NO_SIGNATURE_REQUIRED"
                }
              end
            }
            # xml.Dimensions {
            #   [:length, :width, :height].each do |axis|
            #     name  = axis.to_s.capitalize
            #     value = ((imperial ? pkg.inches(axis) : pkg.cm(axis)).to_f*1000).round/1000.0
            #     xml.name value
            #   end
            #   xml.Units (imperial ? 'IN' : 'CM')
            # }
          }
        end
      }
    }
  end
  builder.to_xml
end
build_ship_request(origin, destination, packages, options={}) click to toggle source
# File lib/omniship/carriers/fedex.rb, line 186
def build_ship_request(origin, destination, packages, options={})
  imperial = ['US','LR','MM'].include?(origin.country_code(:alpha2))

  builder = Nokogiri::XML::Builder.new do |xml|
    xml.ProcessShipmentRequest('xmlns' => 'http://fedex.com/ws/ship/v12') {
      build_access_request(xml)
      xml.Version {
        xml.ServiceId "ship"
        xml.Major "12"
        xml.Intermediate "0"
        xml.Minor "0"
      }
      xml.RequestedShipment {
        xml.ShipTimestamp options[:ship_date] || DateTime.now.strftime
        xml.DropoffType options[:dropoff_type] || 'REGULAR_PICKUP'
        xml.ServiceType options[:service_type] || 'GROUND_HOME_DELIVERY'
        xml.PackagingType options[:package_type] || 'YOUR_PACKAGING'
        build_location_node(["Shipper"], (options[:shipper] || origin), xml)
        build_location_node(["Recipient"], destination, xml)
        if options[:shipper] && options[:shipper] != origin
          build_location_node(["Origin"], origin, xml)
        end
        xml.ShippingChargesPayment {
          xml.PaymentType "SENDER"
          xml.Payor {
            xml.ResponsibleParty {
              xml.AccountNumber @options[:account]
              xml.Contact nil
            }
          }
        }
        xml.SpecialServicesRequested {
          xml.SpecialServiceTypes "SATURDAY_DELIVERY" if options[:saturday_delivery]
          if options[:return_shipment]
            xml.SpecialServiceTypes "RETURN_SHIPMENT"
            xml.ReturnShipmentDetail {
              xml.ReturnType "PRINT_RETURN_LABEL"
            }
          end
        }
        # TODO: Add options to change the label specifications
        xml.LabelSpecification {
          xml.LabelFormatType 'COMMON2D'
          xml.ImageType 'PDF'
          xml.LabelStockType 'PAPER_7X4.75'
        }
        xml.RateRequestTypes 'ACCOUNT'
        xml.PackageCount packages.size
        packages.each do |pkg|
          xml.RequestedPackageLineItems {
            xml.SequenceNumber 1
            xml.Weight {
              xml.Units (imperial ? 'LB' : 'KG')
              xml.Value ((imperial ? pkg.weight : pkg.weight/2.2).to_f)
            }
            xml.SpecialServicesRequested {
              if options[:without_signature]
                xml.SpecialServiceTypes "SIGNATURE_OPTION"
                xml.SignatureOptionDetail {
                  xml.OptionType "NO_SIGNATURE_REQUIRED"
                }
              end
            }
            # xml.Dimensions {
            #   [:length, :width, :height].each do |axis|
            #     name  = axis.to_s.capitalize
            #     value = ((imperial ? pkg.inches(axis) : pkg.cm(axis)).to_f*1000).round/1000.0
            #     xml.send name, value.to_s
            #   end
            #   xml.Units (imperial ? 'IN' : 'CM')
            # }
          }
        end

        if !!@options[:notifications]
          xml.SpecialServicesRequested {
            xml.SpecialServiceTypes "EMAIL_NOTIFICATION"
            xml.EmailNotificationDetail {
              xml.PersonalMessage # Personal Message to be sent to all recipients
              @options[:notifications].each do |email|
                xml.Recipients {
                  xml.EmailAddress email.address
                  xml.NotificationEventsRequested {
                    xml.EmailNotificationEventType{
                      xml.ON_DELIVERY  if email.on_delivery
                      xml.ON_EXCEPTION if email.on_exception
                      xml.ON_SHIPMENT  if email.on_shipment
                      xml.ON_TENDER    if email.on_tender
                    }
                  }
                  xml.Format email.format || "HTML" # options are "HTML" "Text" "Wireless"
                  xml.Localization {
                    xml.Language email.language || "EN" # Default to EN (English)
                    xml.LocaleCode email.locale_code if !email.locale_code.nil?
                  }
                }
              end
              xml.EMailNotificationAggregationType @options[:notification_aggregation_type] if @options.has_key?(:notification_aggregation_type)
            }
          }
        end
      }
    }
  end
  builder.to_xml
end
build_tracking_request(tracking_number, options={}) click to toggle source
# File lib/omniship/carriers/fedex.rb, line 314
def build_tracking_request(tracking_number, options={})
  xml_request = XmlNode.new('TrackRequest', 'xmlns' => 'http://fedex.com/ws/track/v3') do |root_node|
    root_node << build_request_header

    # Version
    root_node << XmlNode.new('Version') do |version_node|
      version_node << XmlNode.new('ServiceId', 'trck')
      version_node << XmlNode.new('Major', '3')
      version_node << XmlNode.new('Intermediate', '0')
      version_node << XmlNode.new('Minor', '0')
    end

    root_node << XmlNode.new('PackageIdentifier') do |package_node|
      package_node << XmlNode.new('Value', tracking_number)
      package_node << XmlNode.new('Type', PackageIdentifierTypes[options['package_identifier_type'] || 'tracking_number'])
    end

    root_node << XmlNode.new('ShipDateRangeBegin', options['ship_date_range_begin']) if options['ship_date_range_begin']
    root_node << XmlNode.new('ShipDateRangeEnd', options['ship_date_range_end']) if options['ship_date_range_end']
    root_node << XmlNode.new('IncludeDetailedScans', 1)
  end
  xml_request.to_s
end
commit(request, test = false) click to toggle source
# File lib/omniship/carriers/fedex.rb, line 493
def commit(request, test = false)
  ssl_post(test ? TEST_URL : LIVE_URL, request)
end
handle_uk_currency(currency) click to toggle source
# File lib/omniship/carriers/fedex.rb, line 497
def handle_uk_currency(currency)
  currency =~ /UKL/i ? 'GBP' : currency
end
parse_delete_response(response, options={}) click to toggle source
# File lib/omniship/carriers/fedex.rb, line 423
def parse_delete_response(response, options={})
  xml     = Nokogiri::XML(response).remove_namespaces!
  success = response_success?(xml)
  message = response_message(xml)
  return [success, message]
end
parse_rate_response(origin, destination, packages, response, options) click to toggle source
# File lib/omniship/carriers/fedex.rb, line 377
def parse_rate_response(origin, destination, packages, response, options)
  xml = Nokogiri::XML(response).remove_namespaces!
  puts xml
  rate_estimates   = []
  success, message = nil

  success = response_success?(xml)
  message = response_message(xml)

  xml.xpath('//RateReplyDetails').each do |rate|
    rate_estimates << RateEstimate.new(origin, destination, @@name,
                      :service_code     => rate.xpath('ServiceType').text,
                      :service_name     => rate.xpath('AppliedOptions').text == "SATURDAY_DELIVERY" ? "#{self.class.service_name_for_code(rate.xpath('ServiceType').text + '_SATURDAY_DELIVERY')}".upcase : self.class.service_name_for_code(rate.xpath('ServiceType').text),
                      :total_price      => rate.xpath('RatedShipmentDetails').first.xpath('ShipmentRateDetail/TotalNetCharge/Amount').text.to_f,
                      :currency         => handle_uk_currency(rate.xpath('RatedShipmentDetails').first.xpath('ShipmentRateDetail/TotalNetCharge/Currency').text),
                      :packages         => packages,
                      :delivery_date    => rate.xpath('ServiceType').text == "FEDEX_GROUND" ? rate.xpath('TransitTime').text : rate.xpath('DeliveryTimestamp').text
                      )
  end

  if rate_estimates.empty?
    success = false
    message = "No shipping rates could be found for the destination address" if message.blank?
  end

  RateResponse.new(success, message, Hash.from_xml(response), :rates => rate_estimates, :xml => response, :request => last_request, :log_xml => options[:log_xml])
end
parse_ship_response(response, options) click to toggle source
# File lib/omniship/carriers/fedex.rb, line 405
def parse_ship_response(response, options)
  xml             = Nokogiri::XML(response).remove_namespaces!
  puts xml
  success         = response_success?(xml)
  message         = response_message(xml)
  label           = nil
  tracking_number = nil

  if success
    label           = xml.xpath("//Image").text
    tracking_number = xml.xpath("//TrackingNumber").text
  else
    success = false
    message = "Shipment was not succcessful." if message.blank?
  end
  ShipResponse.new(success, message, :tracking_number => tracking_number, :label_encoded => label)
end
parse_tracking_response(response, options) click to toggle source
# File lib/omniship/carriers/fedex.rb, line 430
def parse_tracking_response(response, options)
  xml = REXML::Document.new(response)
  root_node = xml.elements['TrackReply']

  success = response_success?(xml)
  message = response_message(xml)

  if success
    tracking_number, origin, destination = nil
    shipment_events = []

    tracking_details = root_node.elements['TrackDetails']
    tracking_number = tracking_details.get_text('TrackingNumber').to_s

    destination_node = tracking_details.elements['DestinationAddress']
    destination = Address.new(
          :country =>     destination_node.get_text('CountryCode').to_s,
          :province =>    destination_node.get_text('StateOrProvinceCode').to_s,
          :city =>        destination_node.get_text('City').to_s
        )

    tracking_details.elements.each('Events') do |event|
      address  = event.elements['Address']

      city     = address.get_text('City').to_s
      state    = address.get_text('StateOrProvinceCode').to_s
      zip_code = address.get_text('PostalCode').to_s
      country  = address.get_text('CountryCode').to_s
      next if country.blank?

      location = Address.new(:city => city, :state => state, :postal_code => zip_code, :country => country)
      description = event.get_text('EventDescription').to_s

      # for now, just assume UTC, even though it probably isn't
      time = Time.parse("#{event.get_text('Timestamp').to_s}")
      zoneless_time = Time.utc(time.year, time.month, time.mday, time.hour, time.min, time.sec)

      shipment_events << ShipmentEvent.new(description, zoneless_time, location)
    end
    shipment_events = shipment_events.sort_by(&:time)
  end

  TrackingResponse.new(success, message, Hash.from_xml(response),
    :xml             => response,
    :request         => last_request,
    :shipment_events => shipment_events,
    :destination     => destination,
    :tracking_number => tracking_number
  )
end
response_message(xml) click to toggle source
# File lib/omniship/carriers/fedex.rb, line 489
def response_message(xml)
  "#{xml.xpath('//Notifications/Severity').text} - #{xml.xpath('//Notifications/Code').text}: #{xml.xpath('//Notifications/Message').text}"
end
response_status_node(xml) click to toggle source
# File lib/omniship/carriers/fedex.rb, line 481
def response_status_node(xml)
  xml.ProcessShipmentReply
end
response_success?(xml) click to toggle source
# File lib/omniship/carriers/fedex.rb, line 485
def response_success?(xml)
  %w{SUCCESS WARNING NOTE}.include? xml.xpath('//Notifications/Severity').text
end