Skip to content
This repository has been archived by the owner on Jun 13, 2018. It is now read-only.

Commit

Permalink
Merge pull request #84 from boontdustie/master
Browse files Browse the repository at this point in the history
Incorporates changes from pull request 48
  • Loading branch information
jiblits committed May 6, 2013
2 parents 00afc26 + c251179 commit af68a1c
Show file tree
Hide file tree
Showing 5 changed files with 477 additions and 111 deletions.
141 changes: 72 additions & 69 deletions lib/active_shipping/shipping/carriers/ups.rb
Expand Up @@ -4,22 +4,22 @@ module ActiveMerchant
module Shipping
class UPS < Carrier
self.retry_safe = true

cattr_accessor :default_options
cattr_reader :name
@@name = "UPS"

TEST_URL = 'https://wwwcie.ups.com'
LIVE_URL = 'https://onlinetools.ups.com'

RESOURCES = {
:rates => 'ups.app/xml/Rate',
:track => 'ups.app/xml/Track'
}

PICKUP_CODES = HashWithIndifferentAccess.new({
:daily_pickup => "01",
:customer_counter => "03",
:customer_counter => "03",
:one_time_pickup => "06",
:on_call_air => "07",
:suggested_retail_rates => "11",
Expand All @@ -29,7 +29,7 @@ class UPS < Carrier

CUSTOMER_CLASSIFICATIONS = HashWithIndifferentAccess.new({
:wholesale => "01",
:occasional => "03",
:occasional => "03",
:retail => "04"
})

Expand All @@ -44,7 +44,7 @@ class UPS < Carrier
:occasional
end
end

DEFAULT_SERVICES = {
"01" => "UPS Next Day Air",
"02" => "UPS Second Day Air",
Expand All @@ -64,24 +64,24 @@ class UPS < Carrier
"85" => "UPS Today Express",
"86" => "UPS Today Express Saver"
}

CANADA_ORIGIN_SERVICES = {
"01" => "UPS Express",
"02" => "UPS Expedited",
"14" => "UPS Express Early A.M."
}

MEXICO_ORIGIN_SERVICES = {
"07" => "UPS Express",
"08" => "UPS Expedited",
"54" => "UPS Express Plus"
}

EU_ORIGIN_SERVICES = {
"07" => "UPS Express",
"08" => "UPS Expedited"
}

OTHER_NON_US_ORIGIN_SERVICES = {
"07" => "UPS Express"
}
Expand All @@ -96,13 +96,13 @@ class UPS < Carrier

# From http://en.wikipedia.org/w/index.php?title=European_Union&oldid=174718707 (Current as of November 30, 2007)
EU_COUNTRY_CODES = ["GB", "AT", "BE", "BG", "CY", "CZ", "DK", "EE", "FI", "FR", "DE", "GR", "HU", "IE", "IT", "LV", "LT", "LU", "MT", "NL", "PL", "PT", "RO", "SK", "SI", "ES", "SE"]

US_TERRITORIES_TREATED_AS_COUNTRIES = ["AS", "FM", "GU", "MH", "MP", "PW", "PR", "VI"]

def requirements
[:key, :login, :password]
end

def find_rates(origin, destination, packages, options={})
origin, destination = upsified_location(origin), upsified_location(destination)
options = @options.merge(options)
Expand All @@ -112,17 +112,17 @@ def find_rates(origin, destination, packages, options={})
response = commit(:rates, save_request(access_request + rate_request), (options[:test] || false))
parse_rate_response(origin, destination, packages, response, options)
end

def find_tracking_info(tracking_number, options={})
options = @options.update(options)
access_request = build_access_request
tracking_request = build_tracking_request(tracking_number, options)
response = commit(:track, save_request(access_request + tracking_request), (options[:test] || false))
parse_tracking_response(response, options)
end

protected

def upsified_location(location)
if location.country_code == 'US' && US_TERRITORIES_TREATED_AS_COUNTRIES.include?(location.state)
atts = {:country => location.state}
Expand All @@ -134,7 +134,7 @@ def upsified_location(location)
location
end
end

def build_access_request
xml_request = XmlNode.new('AccessRequest') do |access_request|
access_request << XmlNode.new('AccessLicenseNumber', @options[:key])
Expand All @@ -143,7 +143,7 @@ def build_access_request
end
xml_request.to_s
end

def build_rate_request(origin, destination, packages, options={})
packages = Array(packages)
xml_request = XmlNode.new('RatingServiceSelectionRequest') do |root_node|
Expand All @@ -153,9 +153,9 @@ def build_rate_request(origin, destination, packages, options={})
# not implemented: 'Rate' RequestOption to specify a single service query
# request << XmlNode.new('RequestOption', ((options[:service].nil? or options[:service] == :all) ? 'Shop' : 'Rate'))
end

pickup_type = options[:pickup_type] || :daily_pickup

root_node << XmlNode.new('PickupType') do |pickup_type_node|
pickup_type_node << XmlNode.new('Code', PICKUP_CODES[pickup_type])
# not implemented: PickupType/PickupDetails element
Expand All @@ -164,36 +164,36 @@ def build_rate_request(origin, destination, packages, options={})
root_node << XmlNode.new('CustomerClassification') do |cc_node|
cc_node << XmlNode.new('Code', CUSTOMER_CLASSIFICATIONS[cc])
end

root_node << XmlNode.new('Shipment') do |shipment|
# not implemented: Shipment/Description element
shipment << build_location_node('Shipper', (options[:shipper] || origin), options)
shipment << build_location_node('ShipTo', destination, options)
if options[:shipper] and options[:shipper] != origin
shipment << build_location_node('ShipFrom', origin, options)
end

# not implemented: * Shipment/ShipmentWeight element
# * Shipment/ReferenceNumber element
# * Shipment/Service element
# * Shipment/PickupDate element
# * Shipment/ScheduledDeliveryDate element
# * Shipment/ScheduledDeliveryTime element
# * Shipment/AlternateDeliveryTime element
# * Shipment/DocumentsOnly element
# * Shipment/ReferenceNumber element
# * Shipment/Service element
# * Shipment/PickupDate element
# * Shipment/ScheduledDeliveryDate element
# * Shipment/ScheduledDeliveryTime element
# * Shipment/AlternateDeliveryTime element
# * Shipment/DocumentsOnly element

packages.each do |package|
imperial = ['US','LR','MM'].include?(origin.country_code(:alpha2))

shipment << XmlNode.new("Package") do |package_node|

# not implemented: * Shipment/Package/PackagingType element
# * Shipment/Package/Description element

package_node << XmlNode.new("PackagingType") do |packaging_type|
packaging_type << XmlNode.new("Code", '02')
end

package_node << XmlNode.new("Dimensions") do |dimensions|
dimensions << XmlNode.new("UnitOfMeasurement") do |units|
units << XmlNode.new("Code", imperial ? 'IN' : 'CM')
Expand All @@ -203,33 +203,35 @@ def build_rate_request(origin, destination, packages, options={})
dimensions << XmlNode.new(axis.to_s.capitalize, [value,0.1].max)
end
end

package_node << XmlNode.new("PackageWeight") do |package_weight|
package_weight << XmlNode.new("UnitOfMeasurement") do |units|
units << XmlNode.new("Code", imperial ? 'LBS' : 'KGS')
end

value = ((imperial ? package.lbs : package.kgs).to_f*1000).round/1000.0 # 3 decimals
package_weight << XmlNode.new("Weight", [value,0.1].max)
end

# not implemented: * Shipment/Package/LargePackageIndicator element
# * Shipment/Package/ReferenceNumber element
# * Shipment/Package/PackageServiceOptions element
# * Shipment/Package/AdditionalHandling element
# * Shipment/Package/AdditionalHandling element
end

end

# not implemented: * Shipment/ShipmentServiceOptions element
# * Shipment/RateInformation element

if options[:origin_account]
shipment << XmlNode.new("RateInformation") do |rate_info_node|
rate_info_node << XmlNode.new("NegotiatedRatesIndicator")
end
end
end

end
xml_request.to_s
end

def build_tracking_request(tracking_number, options={})
xml_request = XmlNode.new('TrackRequest') do |root_node|
root_node << XmlNode.new('Request') do |request|
Expand All @@ -240,7 +242,7 @@ def build_tracking_request(tracking_number, options={})
end
xml_request.to_s
end

def build_location_node(name,location,options={})
# not implemented: * Shipment/Shipper/Name element
# * Shipment/(ShipTo|ShipFrom)/CompanyName element
Expand All @@ -249,13 +251,13 @@ def build_location_node(name,location,options={})
location_node = XmlNode.new(name) do |location_node|
location_node << XmlNode.new('PhoneNumber', location.phone.gsub(/[^\d]/,'')) unless location.phone.blank?
location_node << XmlNode.new('FaxNumber', location.fax.gsub(/[^\d]/,'')) unless location.fax.blank?

if name == 'Shipper' and (origin_account = @options[:origin_account] || options[:origin_account])
location_node << XmlNode.new('ShipperNumber', origin_account)
elsif name == 'ShipTo' and (destination_account = @options[:destination_account] || options[:destination_account])
location_node << XmlNode.new('ShipperAssignedIdentificationNumber', destination_account)
end

location_node << XmlNode.new('Address') do |address|
address << XmlNode.new("AddressLine1", location.address1) unless location.address1.blank?
address << XmlNode.new("AddressLine2", location.address2) unless location.address2.blank?
Expand All @@ -270,17 +272,17 @@ def build_location_node(name,location,options={})
end
end
end

def parse_rate_response(origin, destination, packages, response, options={})
rates = []

xml = REXML::Document.new(response)
success = response_success?(xml)
message = response_message(xml)

if success
rate_estimates = []

xml.elements.each('/*/RatedShipment') do |rated_shipment|
service_code = rated_shipment.get_text('Service/Code').to_s
days_to_delivery = rated_shipment.get_text('GuaranteedDaysToDelivery').to_s.to_i
Expand All @@ -292,17 +294,18 @@ def parse_rate_response(origin, destination, packages, response, options={})
:currency => rated_shipment.get_text('TotalCharges/CurrencyCode').to_s,
:service_code => service_code,
:packages => packages,
:delivery_range => [timestamp_from_business_day(days_to_delivery)])
:delivery_range => [timestamp_from_business_day(days_to_delivery)],
:negotiated_rate => rated_shipment.get_text('NegotiatedRates/NetSummaryCharges/GrandTotal/MonetaryValue').to_s.to_f)
end
end
RateResponse.new(success, message, Hash.from_xml(response).values.first, :rates => rate_estimates, :xml => response, :request => last_request)
end

def parse_tracking_response(response, options={})
xml = REXML::Document.new(response)
success = response_success?(xml)
message = response_message(xml)

if success
tracking_number, origin, destination, status_code, status_description = nil
delivered, exception = false
Expand All @@ -314,7 +317,7 @@ def parse_tracking_response(response, options={})
first_shipment = xml.elements['/*/Shipment']
first_package = first_shipment.elements['Package']
tracking_number = first_shipment.get_text('ShipmentIdentificationNumber | Package/TrackingNumber').to_s

# Build status hash
status_node = first_package.elements['Activity/Status/StatusType']
status_code = status_node.get_text('Code').to_s
Expand Down Expand Up @@ -351,10 +354,10 @@ def parse_tracking_response(response, options={})
location = location_from_address_node(activity.elements['ActivityLocation/Address'])
ShipmentEvent.new(description, zoneless_time, location)
end

shipment_events = shipment_events.sort_by(&:time)
# UPS will sometimes archive a shipment, stripping all shipment activity except for the delivery

# UPS will sometimes archive a shipment, stripping all shipment activity except for the delivery
# event (see test/fixtures/xml/delivered_shipment_without_events_tracking_response.xml for an example).
# This adds an origin event to the shipment activity in such cases.
if origin && !(shipment_events.count == 1 && status == :delivered)
Expand All @@ -377,7 +380,7 @@ def parse_tracking_response(response, options={})
shipment_events[-1] = ShipmentEvent.new(shipment_events.last.name, shipment_events.last.time, destination)
end
end

end
TrackingResponse.new(success, message, Hash.from_xml(response).values.first,
:carrier => @@name,
Expand All @@ -395,7 +398,7 @@ def parse_tracking_response(response, options={})
:destination => destination,
:tracking_number => tracking_number)
end

def location_from_address_node(address)
return nil unless address
Location.new(
Expand All @@ -408,7 +411,7 @@ def location_from_address_node(address)
:address3 => node_text_or_nil(address.elements['AddressLine3'])
)
end

def parse_ups_datetime(options = {})
time, date = options[:time].to_s, options[:date].to_s
if time.nil?
Expand All @@ -424,29 +427,29 @@ def parse_ups_datetime(options = {})
def response_success?(xml)
xml.get_text('/*/Response/ResponseStatusCode').to_s == '1'
end

def response_message(xml)
xml.get_text('/*/Response/Error/ErrorDescription | /*/Response/ResponseStatusDescription').to_s
end

def commit(action, request, test = false)
ssl_post("#{test ? TEST_URL : LIVE_URL}/#{RESOURCES[action]}", request)
end


def service_name_for(origin, code)
origin = origin.country_code(:alpha2)

name = case origin
when "CA" then CANADA_ORIGIN_SERVICES[code]
when "MX" then MEXICO_ORIGIN_SERVICES[code]
when *EU_COUNTRY_CODES then EU_ORIGIN_SERVICES[code]
end

name ||= OTHER_NON_US_ORIGIN_SERVICES[code] unless name == 'US'
name ||= DEFAULT_SERVICES[code]
end

end
end
end

0 comments on commit af68a1c

Please sign in to comment.