omniship 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +0 -0
- data/MIT-LICENSE +20 -0
- data/README.markdown +91 -0
- data/lib/omniship/address.rb +135 -0
- data/lib/omniship/base.rb +11 -0
- data/lib/omniship/carrier.rb +69 -0
- data/lib/omniship/carriers/fedex.rb +330 -0
- data/lib/omniship/carriers/ups.rb +564 -0
- data/lib/omniship/carriers/usps.rb +438 -0
- data/lib/omniship/carriers.rb +13 -0
- data/lib/omniship/contact.rb +19 -0
- data/lib/omniship/package.rb +147 -0
- data/lib/omniship/rate_estimate.rb +59 -0
- data/lib/omniship/rate_response.rb +16 -0
- data/lib/omniship/response.rb +42 -0
- data/lib/omniship/shipment_event.rb +12 -0
- data/lib/omniship/tracking_response.rb +18 -0
- data/lib/omniship/version.rb +3 -0
- data/lib/omniship.rb +73 -0
- data/lib/vendor/quantified/MIT-LICENSE +22 -0
- data/lib/vendor/quantified/README.markdown +49 -0
- data/lib/vendor/quantified/Rakefile +21 -0
- data/lib/vendor/quantified/init.rb +0 -0
- data/lib/vendor/quantified/lib/quantified/attribute.rb +208 -0
- data/lib/vendor/quantified/lib/quantified/length.rb +20 -0
- data/lib/vendor/quantified/lib/quantified/mass.rb +19 -0
- data/lib/vendor/quantified/lib/quantified.rb +6 -0
- data/lib/vendor/quantified/test/length_test.rb +92 -0
- data/lib/vendor/quantified/test/mass_test.rb +88 -0
- data/lib/vendor/quantified/test/test_helper.rb +10 -0
- data/lib/vendor/xml_node/README +36 -0
- data/lib/vendor/xml_node/Rakefile +21 -0
- data/lib/vendor/xml_node/benchmark/bench_generation.rb +32 -0
- data/lib/vendor/xml_node/init.rb +1 -0
- data/lib/vendor/xml_node/lib/xml_node.rb +222 -0
- data/lib/vendor/xml_node/test/test_generating.rb +94 -0
- data/lib/vendor/xml_node/test/test_parsing.rb +43 -0
- metadata +205 -0
data/CHANGELOG
ADDED
File without changes
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2012 Donavan White
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOa AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SaALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.markdown
ADDED
@@ -0,0 +1,91 @@
|
|
1
|
+
[![Code
|
2
|
+
Climate](https://codeclimate.com/badge.png)](https://codeclimate.com/github/Digi-Cazter/omniship)
|
3
|
+
|
4
|
+
# Omniship
|
5
|
+
|
6
|
+
This gem is under active development, I'm only in the Alpha stage right now, so keep checking back for updates.
|
7
|
+
|
8
|
+
This library has been created to make web requests to common shipping carriers using XML. I created this to be easy to use with a nice Ruby API. This code was originally forked from the *Shopify/active_shipping* code, I began to strip it down cause I wan't a cleaner API along with the ability to actually create shipment labels with it. After changing enough code, I created this gem as its own project since it's different enough.
|
9
|
+
|
10
|
+
## Supported Shipping Carriers
|
11
|
+
|
12
|
+
* [UPS](http://www.ups.com)
|
13
|
+
* [USPS](http://www.usps.com) COMING SOON!
|
14
|
+
* [FedEx](http://www.fedex.com) COMING SOON!
|
15
|
+
|
16
|
+
## Simple example snippets
|
17
|
+
### UPS Code Example ###
|
18
|
+
To run in test mode during development, pass :test => true as an option
|
19
|
+
into create_shipment and accept_shipment.
|
20
|
+
|
21
|
+
def create_shipment
|
22
|
+
# If you have created the omniship.yml config file
|
23
|
+
@config = OMNISHIP_CONFIG[Rails.env]['ups']
|
24
|
+
shipment = create_ups_shipment
|
25
|
+
end
|
26
|
+
|
27
|
+
def create_ups_shipment
|
28
|
+
# If using the yml config
|
29
|
+
ups = Omniship::UPS.new
|
30
|
+
# Else just pass in the credentials
|
31
|
+
ups = Omniship::UPS.new(:login => @user, :password => @password, :key => @key)
|
32
|
+
send_options = {}
|
33
|
+
send_options[:origin_account] = @config["account"] # Or just put the shipper account here
|
34
|
+
send_options[:service] = "03"
|
35
|
+
response = ups.create_shipment(origin, destination, package, options = send_options)
|
36
|
+
return ups.accept_shipment(response)
|
37
|
+
end
|
38
|
+
|
39
|
+
def origin
|
40
|
+
address = {}
|
41
|
+
address[:name] = "My House"
|
42
|
+
address[:address1] = "555 Diagonal"
|
43
|
+
address[:city] = "Saint George"
|
44
|
+
address[:state] = "UT"
|
45
|
+
address[:zip] = "84770"
|
46
|
+
address[:country] = "USA"
|
47
|
+
return Omniship::Address.new(address)
|
48
|
+
end
|
49
|
+
|
50
|
+
def destination
|
51
|
+
address = {}
|
52
|
+
address[:company_name] = "Wal-Mart"
|
53
|
+
address[:address1] = "555 Diagonal"
|
54
|
+
address[:city] = "Saint George"
|
55
|
+
address[:state] = "UT"
|
56
|
+
address[:zip] = "84770"
|
57
|
+
address[:country] = "USA"
|
58
|
+
return Omniship::Address.new(address)
|
59
|
+
end
|
60
|
+
|
61
|
+
def packages
|
62
|
+
# UPS can handle a single package or multiple packages
|
63
|
+
pkg_list = []
|
64
|
+
weight = 1
|
65
|
+
length = 1
|
66
|
+
width = 1
|
67
|
+
height = 1
|
68
|
+
package_type = "02"
|
69
|
+
pkg_list << Omniship::Package.new(weight.to_i,[length.to_i,width.to_i,height.to_i],:units => :imperial, :package_type => package_type)
|
70
|
+
return pkg_list
|
71
|
+
end
|
72
|
+
|
73
|
+
## Tests
|
74
|
+
|
75
|
+
Currently this is on my TODO list. Check back for updates
|
76
|
+
|
77
|
+
## Contributing
|
78
|
+
|
79
|
+
Before anyone starts contributing, I want to get a good stable version going and tests to follow, after I get that going then for the features you add, you should have both unit tests and remote tests. It's probably best to start with the remote tests, and then log those requests and responses and use them as the mocks for the unit tests.
|
80
|
+
|
81
|
+
To log requests and responses, just set the `logger` on your carrier class to some kind of `Logger` object:
|
82
|
+
|
83
|
+
Omniship::USPS.logger = Logger.new($stdout)
|
84
|
+
|
85
|
+
(This logging functionality is provided by the [`PostsData` module](https://github.com/Shopify/active_utils/blob/master/lib/active_utils/common/posts_data.rb) in the `active_utils` dependency.)
|
86
|
+
|
87
|
+
After you've pushed your well-tested changes to your github fork, make a pull request and we'll take it from there!
|
88
|
+
|
89
|
+
## Legal Mumbo Jumbo
|
90
|
+
|
91
|
+
Unless otherwise noted in specific files, all code in the Omniship project is under the copyright and license described in the included MIT-LICENSE file.
|
@@ -0,0 +1,135 @@
|
|
1
|
+
module Omniship #:nodoc:
|
2
|
+
class Address
|
3
|
+
ADDRESS_TYPES = %w{residential commercial po_box}
|
4
|
+
|
5
|
+
attr_reader :options,
|
6
|
+
:country,
|
7
|
+
:postal_code,
|
8
|
+
:province,
|
9
|
+
:city,
|
10
|
+
:name,
|
11
|
+
:attention_name,
|
12
|
+
:address1,
|
13
|
+
:address2,
|
14
|
+
:address3,
|
15
|
+
:phone,
|
16
|
+
:fax,
|
17
|
+
:address_type,
|
18
|
+
:company_name
|
19
|
+
|
20
|
+
alias_method :zip, :postal_code
|
21
|
+
alias_method :postal, :postal_code
|
22
|
+
alias_method :state, :province
|
23
|
+
alias_method :territory, :province
|
24
|
+
alias_method :region, :province
|
25
|
+
alias_method :company, :company_name
|
26
|
+
|
27
|
+
def initialize(options = {})
|
28
|
+
@country = (options[:country].nil? or options[:country].is_a?(ActiveMerchant::Country)) ?
|
29
|
+
options[:country] :
|
30
|
+
ActiveMerchant::Country.find(options[:country])
|
31
|
+
@postal_code = options[:postal_code] || options[:postal] || options[:zip]
|
32
|
+
@province = options[:province] || options[:state] || options[:territory] || options[:region]
|
33
|
+
@city = options[:city]
|
34
|
+
@name = options[:name]
|
35
|
+
@address1 = options[:address1]
|
36
|
+
@address2 = options[:address2]
|
37
|
+
@address3 = options[:address3]
|
38
|
+
@phone = options[:phone]
|
39
|
+
@fax = options[:fax]
|
40
|
+
@company_name = options[:company_name] || options[:company]
|
41
|
+
|
42
|
+
self.address_type = options[:address_type]
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.from(object, options={})
|
46
|
+
return object if object.is_a? Omniship::Address
|
47
|
+
attr_mappings = {
|
48
|
+
:name => [:name],
|
49
|
+
:country => [:country_code, :country],
|
50
|
+
:postal_code => [:postal_code, :zip, :postal],
|
51
|
+
:province => [:province_code, :state_code, :territory_code, :region_code, :province, :state, :territory, :region],
|
52
|
+
:city => [:city, :town],
|
53
|
+
:address1 => [:address1, :address, :street],
|
54
|
+
:address2 => [:address2],
|
55
|
+
:address3 => [:address3],
|
56
|
+
:phone => [:phone, :phone_number],
|
57
|
+
:fax => [:fax, :fax_number],
|
58
|
+
:address_type => [:address_type],
|
59
|
+
:company_name => [:company, :company_name]
|
60
|
+
}
|
61
|
+
attributes = {}
|
62
|
+
hash_access = begin
|
63
|
+
object[:some_symbol]
|
64
|
+
true
|
65
|
+
rescue
|
66
|
+
false
|
67
|
+
end
|
68
|
+
attr_mappings.each do |pair|
|
69
|
+
pair[1].each do |sym|
|
70
|
+
if value = (object[sym] if hash_access) || (object.send(sym) if object.respond_to?(sym) && (!hash_access || !Hash.public_instance_methods.include?(sym.to_s)))
|
71
|
+
attributes[pair[0]] = value
|
72
|
+
break
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
attributes.delete(:address_type) unless ADDRESS_TYPES.include?(attributes[:address_type].to_s)
|
77
|
+
self.new(attributes.update(options))
|
78
|
+
end
|
79
|
+
|
80
|
+
def country_code(format = :alpha2)
|
81
|
+
@country.nil? ? nil : @country.code(format).value
|
82
|
+
end
|
83
|
+
|
84
|
+
def residential?; @address_type == 'residential' end
|
85
|
+
def commercial?; @address_type == 'commercial' end
|
86
|
+
def po_box?; @address_type == 'po_box' end
|
87
|
+
|
88
|
+
def address_type=(value)
|
89
|
+
return unless value.present?
|
90
|
+
raise ArgumentError.new("address_type must be one of #{ADDRESS_TYPES.join(', ')}") unless ADDRESS_TYPES.include?(value.to_s)
|
91
|
+
@address_type = value.to_s
|
92
|
+
end
|
93
|
+
|
94
|
+
def to_hash
|
95
|
+
{
|
96
|
+
:country => country_code,
|
97
|
+
:postal_code => postal_code,
|
98
|
+
:province => province,
|
99
|
+
:city => city,
|
100
|
+
:name => name,
|
101
|
+
:address1 => address1,
|
102
|
+
:address2 => address2,
|
103
|
+
:address3 => address3,
|
104
|
+
:phone => phone,
|
105
|
+
:fax => fax,
|
106
|
+
:address_type => address_type,
|
107
|
+
:company_name => company_name
|
108
|
+
}
|
109
|
+
end
|
110
|
+
|
111
|
+
def to_xml(options={})
|
112
|
+
options[:root] ||= "address"
|
113
|
+
to_hash.to_xml(options)
|
114
|
+
end
|
115
|
+
|
116
|
+
def to_s
|
117
|
+
prettyprint.gsub(/\n/, ' ')
|
118
|
+
end
|
119
|
+
|
120
|
+
def prettyprint
|
121
|
+
chunks = []
|
122
|
+
chunks << [@name, @address1,@address2,@address3].reject {|e| e.blank?}.join("\n")
|
123
|
+
chunks << [@city,@province,@postal_code].reject {|e| e.blank?}.join(', ')
|
124
|
+
chunks << @country
|
125
|
+
chunks.reject {|e| e.blank?}.join("\n")
|
126
|
+
end
|
127
|
+
|
128
|
+
def inspect
|
129
|
+
string = prettyprint
|
130
|
+
string << "\nPhone: #{@phone}" unless @phone.blank?
|
131
|
+
string << "\nFax: #{@fax}" unless @fax.blank?
|
132
|
+
string
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
module Omniship
|
2
|
+
class Carrier
|
3
|
+
|
4
|
+
include ActiveMerchant::RequiresParameters
|
5
|
+
include ActiveMerchant::PostsData
|
6
|
+
include Quantified
|
7
|
+
|
8
|
+
attr_reader :last_request
|
9
|
+
attr_accessor :test_mode
|
10
|
+
alias_method :test_mode?, :test_mode
|
11
|
+
|
12
|
+
# Credentials should be in options hash under keys :login, :password and/or :key.
|
13
|
+
def initialize(options = {})
|
14
|
+
#requirements.each {|key| requires!(options, key)}
|
15
|
+
@options = options
|
16
|
+
@last_request = nil
|
17
|
+
@test_mode = @options[:test]
|
18
|
+
@config = Omniship.setup
|
19
|
+
end
|
20
|
+
|
21
|
+
# Override to return required keys in options hash for initialize method.
|
22
|
+
def requirements
|
23
|
+
[]
|
24
|
+
end
|
25
|
+
|
26
|
+
# Override with whatever you need to get the rates
|
27
|
+
def find_rates(origin, destination, packages, options = {})
|
28
|
+
end
|
29
|
+
|
30
|
+
# Validate credentials with a call to the API. By default this just does a find_rates call
|
31
|
+
# with the orgin and destination both as the carrier's default_location. Override to provide
|
32
|
+
# alternate functionality, such as checking for test_mode to use test servers, etc.
|
33
|
+
def valid_credentials?
|
34
|
+
location = self.class.default_location
|
35
|
+
find_rates(location,location,Package.new(100, [5,15,30]), :test => test_mode)
|
36
|
+
rescue Omniship::ResponseError
|
37
|
+
false
|
38
|
+
else
|
39
|
+
true
|
40
|
+
end
|
41
|
+
|
42
|
+
def maximum_weight
|
43
|
+
Mass.new(150, :pounds)
|
44
|
+
end
|
45
|
+
|
46
|
+
protected
|
47
|
+
|
48
|
+
def node_text_or_nil(xml_node)
|
49
|
+
xml_node ? xml_node.text : nil
|
50
|
+
end
|
51
|
+
|
52
|
+
# Override in subclasses for non-U.S.-based carriers.
|
53
|
+
def self.default_location
|
54
|
+
Address.new( :country => 'US',
|
55
|
+
:state => 'CA',
|
56
|
+
:city => 'Beverly Hills',
|
57
|
+
:address1 => '455 N. Rexford Dr.',
|
58
|
+
:address2 => '3rd Floor',
|
59
|
+
:zip => '90210',
|
60
|
+
:phone => '1-310-285-1013',
|
61
|
+
:fax => '1-310-275-8159')
|
62
|
+
end
|
63
|
+
|
64
|
+
# Use after building the request to save for later inspection. Probably won't ever be overridden.
|
65
|
+
def save_request(r)
|
66
|
+
@last_request = r
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,330 @@
|
|
1
|
+
# FedEx module by Jimmy Baker
|
2
|
+
# http://github.com/jimmyebaker
|
3
|
+
|
4
|
+
module Omniship
|
5
|
+
# :key is your developer API key
|
6
|
+
# :password is your API password
|
7
|
+
# :account is your FedEx account number
|
8
|
+
# :login is your meter number
|
9
|
+
class FedEx < Carrier
|
10
|
+
self.retry_safe = true
|
11
|
+
|
12
|
+
cattr_reader :name
|
13
|
+
@@name = "FedEx"
|
14
|
+
|
15
|
+
TEST_URL = 'https://gatewaybeta.fedex.com:443/xml'
|
16
|
+
LIVE_URL = 'https://gateway.fedex.com:443/xml'
|
17
|
+
|
18
|
+
CarrierCodes = {
|
19
|
+
"fedex_ground" => "FDXG",
|
20
|
+
"fedex_express" => "FDXE"
|
21
|
+
}
|
22
|
+
|
23
|
+
ServiceTypes = {
|
24
|
+
"PRIORITY_OVERNIGHT" => "FedEx Priority Overnight",
|
25
|
+
"PRIORITY_OVERNIGHT_SATURDAY_DELIVERY" => "FedEx Priority Overnight Saturday Delivery",
|
26
|
+
"FEDEX_2_DAY" => "FedEx 2 Day",
|
27
|
+
"FEDEX_2_DAY_SATURDAY_DELIVERY" => "FedEx 2 Day Saturday Delivery",
|
28
|
+
"STANDARD_OVERNIGHT" => "FedEx Standard Overnight",
|
29
|
+
"FIRST_OVERNIGHT" => "FedEx First Overnight",
|
30
|
+
"FIRST_OVERNIGHT_SATURDAY_DELIVERY" => "FedEx First Overnight Saturday Delivery",
|
31
|
+
"FEDEX_EXPRESS_SAVER" => "FedEx Express Saver",
|
32
|
+
"FEDEX_1_DAY_FREIGHT" => "FedEx 1 Day Freight",
|
33
|
+
"FEDEX_1_DAY_FREIGHT_SATURDAY_DELIVERY" => "FedEx 1 Day Freight Saturday Delivery",
|
34
|
+
"FEDEX_2_DAY_FREIGHT" => "FedEx 2 Day Freight",
|
35
|
+
"FEDEX_2_DAY_FREIGHT_SATURDAY_DELIVERY" => "FedEx 2 Day Freight Saturday Delivery",
|
36
|
+
"FEDEX_3_DAY_FREIGHT" => "FedEx 3 Day Freight",
|
37
|
+
"FEDEX_3_DAY_FREIGHT_SATURDAY_DELIVERY" => "FedEx 3 Day Freight Saturday Delivery",
|
38
|
+
"INTERNATIONAL_PRIORITY" => "FedEx International Priority",
|
39
|
+
"INTERNATIONAL_PRIORITY_SATURDAY_DELIVERY" => "FedEx International Priority Saturday Delivery",
|
40
|
+
"INTERNATIONAL_ECONOMY" => "FedEx International Economy",
|
41
|
+
"INTERNATIONAL_FIRST" => "FedEx International First",
|
42
|
+
"INTERNATIONAL_PRIORITY_FREIGHT" => "FedEx International Priority Freight",
|
43
|
+
"INTERNATIONAL_ECONOMY_FREIGHT" => "FedEx International Economy Freight",
|
44
|
+
"GROUND_HOME_DELIVERY" => "FedEx Ground Home Delivery",
|
45
|
+
"FEDEX_GROUND" => "FedEx Ground",
|
46
|
+
"INTERNATIONAL_GROUND" => "FedEx International Ground"
|
47
|
+
}
|
48
|
+
|
49
|
+
PackageTypes = {
|
50
|
+
"fedex_envelope" => "FEDEX_ENVELOPE",
|
51
|
+
"fedex_pak" => "FEDEX_PAK",
|
52
|
+
"fedex_box" => "FEDEX_BOX",
|
53
|
+
"fedex_tube" => "FEDEX_TUBE",
|
54
|
+
"fedex_10_kg_box" => "FEDEX_10KG_BOX",
|
55
|
+
"fedex_25_kg_box" => "FEDEX_25KG_BOX",
|
56
|
+
"your_packaging" => "YOUR_PACKAGING"
|
57
|
+
}
|
58
|
+
|
59
|
+
DropoffTypes = {
|
60
|
+
'regular_pickup' => 'REGULAR_PICKUP',
|
61
|
+
'request_courier' => 'REQUEST_COURIER',
|
62
|
+
'dropbox' => 'DROP_BOX',
|
63
|
+
'business_service_center' => 'BUSINESS_SERVICE_CENTER',
|
64
|
+
'station' => 'STATION'
|
65
|
+
}
|
66
|
+
|
67
|
+
PaymentTypes = {
|
68
|
+
'sender' => 'SENDER',
|
69
|
+
'recipient' => 'RECIPIENT',
|
70
|
+
'third_party' => 'THIRDPARTY',
|
71
|
+
'collect' => 'COLLECT'
|
72
|
+
}
|
73
|
+
|
74
|
+
PackageIdentifierTypes = {
|
75
|
+
'tracking_number' => 'TRACKING_NUMBER_OR_DOORTAG',
|
76
|
+
'door_tag' => 'TRACKING_NUMBER_OR_DOORTAG',
|
77
|
+
'rma' => 'RMA',
|
78
|
+
'ground_shipment_id' => 'GROUND_SHIPMENT_ID',
|
79
|
+
'ground_invoice_number' => 'GROUND_INVOICE_NUMBER',
|
80
|
+
'ground_customer_reference' => 'GROUND_CUSTOMER_REFERENCE',
|
81
|
+
'ground_po' => 'GROUND_PO',
|
82
|
+
'express_reference' => 'EXPRESS_REFERENCE',
|
83
|
+
'express_mps_master' => 'EXPRESS_MPS_MASTER'
|
84
|
+
}
|
85
|
+
|
86
|
+
def self.service_name_for_code(service_code)
|
87
|
+
ServiceTypes[service_code] || begin
|
88
|
+
name = service_code.downcase.split('_').collect{|word| word.capitalize }.join(' ')
|
89
|
+
"FedEx #{name.sub(/Fedex /, '')}"
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def requirements
|
94
|
+
[:key, :password, :account, :login]
|
95
|
+
end
|
96
|
+
|
97
|
+
def find_rates(origin, destination, packages, options = {})
|
98
|
+
options = @options.update(options)
|
99
|
+
packages = Array(packages)
|
100
|
+
|
101
|
+
rate_request = build_rate_request(origin, destination, packages, options)
|
102
|
+
|
103
|
+
response = commit(save_request(rate_request), (options[:test] || false)).gsub(/<(\/)?.*?\:(.*?)>/, '<\1\2>')
|
104
|
+
|
105
|
+
parse_rate_response(origin, destination, packages, response, options)
|
106
|
+
end
|
107
|
+
|
108
|
+
def find_tracking_info(tracking_number, options={})
|
109
|
+
options = @options.update(options)
|
110
|
+
|
111
|
+
tracking_request = build_tracking_request(tracking_number, options)
|
112
|
+
response = commit(save_request(tracking_request), (options[:test] || false)).gsub(/<(\/)?.*?\:(.*?)>/, '<\1\2>')
|
113
|
+
parse_tracking_response(response, options)
|
114
|
+
end
|
115
|
+
|
116
|
+
protected
|
117
|
+
def build_rate_request(origin, destination, packages, options={})
|
118
|
+
imperial = ['US','LR','MM'].include?(origin.country_code(:alpha2))
|
119
|
+
|
120
|
+
xml_request = XmlNode.new('RateRequest', 'xmlns' => 'http://fedex.com/ws/rate/v6') do |root_node|
|
121
|
+
root_node << build_request_header
|
122
|
+
|
123
|
+
# Version
|
124
|
+
root_node << XmlNode.new('Version') do |version_node|
|
125
|
+
version_node << XmlNode.new('ServiceId', 'crs')
|
126
|
+
version_node << XmlNode.new('Major', '6')
|
127
|
+
version_node << XmlNode.new('Intermediate', '0')
|
128
|
+
version_node << XmlNode.new('Minor', '0')
|
129
|
+
end
|
130
|
+
|
131
|
+
# Returns delivery dates
|
132
|
+
root_node << XmlNode.new('ReturnTransitAndCommit', true)
|
133
|
+
# Returns saturday delivery shipping options when available
|
134
|
+
root_node << XmlNode.new('VariableOptions', 'SATURDAY_DELIVERY')
|
135
|
+
|
136
|
+
root_node << XmlNode.new('RequestedShipment') do |rs|
|
137
|
+
rs << XmlNode.new('ShipTimestamp', Time.now)
|
138
|
+
rs << XmlNode.new('DropoffType', options[:dropoff_type] || 'REGULAR_PICKUP')
|
139
|
+
rs << XmlNode.new('PackagingType', options[:packaging_type] || 'YOUR_PACKAGING')
|
140
|
+
|
141
|
+
rs << build_location_node('Shipper', (options[:shipper] || origin))
|
142
|
+
rs << build_location_node('Recipient', destination)
|
143
|
+
if options[:shipper] and options[:shipper] != origin
|
144
|
+
rs << build_location_node('Origin', origin)
|
145
|
+
end
|
146
|
+
|
147
|
+
rs << XmlNode.new('RateRequestTypes', 'ACCOUNT')
|
148
|
+
rs << XmlNode.new('PackageCount', packages.size)
|
149
|
+
packages.each do |pkg|
|
150
|
+
rs << XmlNode.new('RequestedPackages') do |rps|
|
151
|
+
rps << XmlNode.new('Weight') do |tw|
|
152
|
+
tw << XmlNode.new('Units', imperial ? 'LB' : 'KG')
|
153
|
+
tw << XmlNode.new('Value', [((imperial ? pkg.lbs : pkg.kgs).to_f*1000).round/1000.0, 0.1].max)
|
154
|
+
end
|
155
|
+
rps << XmlNode.new('Dimensions') do |dimensions|
|
156
|
+
[:length,:width,:height].each do |axis|
|
157
|
+
value = ((imperial ? pkg.inches(axis) : pkg.cm(axis)).to_f*1000).round/1000.0 # 3 decimals
|
158
|
+
dimensions << XmlNode.new(axis.to_s.capitalize, value.ceil)
|
159
|
+
end
|
160
|
+
dimensions << XmlNode.new('Units', imperial ? 'IN' : 'CM')
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
end
|
166
|
+
end
|
167
|
+
xml_request.to_s
|
168
|
+
end
|
169
|
+
|
170
|
+
def build_tracking_request(tracking_number, options={})
|
171
|
+
xml_request = XmlNode.new('TrackRequest', 'xmlns' => 'http://fedex.com/ws/track/v3') do |root_node|
|
172
|
+
root_node << build_request_header
|
173
|
+
|
174
|
+
# Version
|
175
|
+
root_node << XmlNode.new('Version') do |version_node|
|
176
|
+
version_node << XmlNode.new('ServiceId', 'trck')
|
177
|
+
version_node << XmlNode.new('Major', '3')
|
178
|
+
version_node << XmlNode.new('Intermediate', '0')
|
179
|
+
version_node << XmlNode.new('Minor', '0')
|
180
|
+
end
|
181
|
+
|
182
|
+
root_node << XmlNode.new('PackageIdentifier') do |package_node|
|
183
|
+
package_node << XmlNode.new('Value', tracking_number)
|
184
|
+
package_node << XmlNode.new('Type', PackageIdentifierTypes[options['package_identifier_type'] || 'tracking_number'])
|
185
|
+
end
|
186
|
+
|
187
|
+
root_node << XmlNode.new('ShipDateRangeBegin', options['ship_date_range_begin']) if options['ship_date_range_begin']
|
188
|
+
root_node << XmlNode.new('ShipDateRangeEnd', options['ship_date_range_end']) if options['ship_date_range_end']
|
189
|
+
root_node << XmlNode.new('IncludeDetailedScans', 1)
|
190
|
+
end
|
191
|
+
xml_request.to_s
|
192
|
+
end
|
193
|
+
|
194
|
+
def build_request_header
|
195
|
+
web_authentication_detail = XmlNode.new('WebAuthenticationDetail') do |wad|
|
196
|
+
wad << XmlNode.new('UserCredential') do |uc|
|
197
|
+
uc << XmlNode.new('Key', @options[:key])
|
198
|
+
uc << XmlNode.new('Password', @options[:password])
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
client_detail = XmlNode.new('ClientDetail') do |cd|
|
203
|
+
cd << XmlNode.new('AccountNumber', @options[:account])
|
204
|
+
cd << XmlNode.new('MeterNumber', @options[:login])
|
205
|
+
end
|
206
|
+
|
207
|
+
trasaction_detail = XmlNode.new('TransactionDetail') do |td|
|
208
|
+
td << XmlNode.new('CustomerTransactionId', 'ActiveShipping') # TODO: Need to do something better with this..
|
209
|
+
end
|
210
|
+
|
211
|
+
[web_authentication_detail, client_detail, trasaction_detail]
|
212
|
+
end
|
213
|
+
|
214
|
+
def build_location_node(name, location)
|
215
|
+
location_node = XmlNode.new(name) do |xml_node|
|
216
|
+
xml_node << XmlNode.new('Address') do |address_node|
|
217
|
+
address_node << XmlNode.new('PostalCode', location.postal_code)
|
218
|
+
address_node << XmlNode.new("CountryCode", location.country_code(:alpha2))
|
219
|
+
|
220
|
+
address_node << XmlNode.new("Residential", true) unless location.commercial?
|
221
|
+
end
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
def parse_rate_response(origin, destination, packages, response, options)
|
226
|
+
rate_estimates = []
|
227
|
+
success, message = nil
|
228
|
+
|
229
|
+
xml = REXML::Document.new(response)
|
230
|
+
root_node = xml.elements['RateReply']
|
231
|
+
|
232
|
+
success = response_success?(xml)
|
233
|
+
message = response_message(xml)
|
234
|
+
|
235
|
+
root_node.elements.each('RateReplyDetails') do |rated_shipment|
|
236
|
+
service_code = rated_shipment.get_text('ServiceType').to_s
|
237
|
+
is_saturday_delivery = rated_shipment.get_text('AppliedOptions').to_s == 'SATURDAY_DELIVERY'
|
238
|
+
service_type = is_saturday_delivery ? "#{service_code}_SATURDAY_DELIVERY" : service_code
|
239
|
+
|
240
|
+
currency = handle_uk_currency(rated_shipment.get_text('RatedShipmentDetails/ShipmentRateDetail/TotalNetCharge/Currency').to_s)
|
241
|
+
rate_estimates << RateEstimate.new(origin, destination, @@name,
|
242
|
+
self.class.service_name_for_code(service_type),
|
243
|
+
:service_code => service_code,
|
244
|
+
:total_price => rated_shipment.get_text('RatedShipmentDetails/ShipmentRateDetail/TotalNetCharge/Amount').to_s.to_f,
|
245
|
+
:currency => currency,
|
246
|
+
:packages => packages,
|
247
|
+
:delivery_range => [rated_shipment.get_text('DeliveryTimestamp').to_s] * 2)
|
248
|
+
end
|
249
|
+
|
250
|
+
if rate_estimates.empty?
|
251
|
+
success = false
|
252
|
+
message = "No shipping rates could be found for the destination address" if message.blank?
|
253
|
+
end
|
254
|
+
|
255
|
+
RateResponse.new(success, message, Hash.from_xml(response), :rates => rate_estimates, :xml => response, :request => last_request, :log_xml => options[:log_xml])
|
256
|
+
end
|
257
|
+
|
258
|
+
def parse_tracking_response(response, options)
|
259
|
+
xml = REXML::Document.new(response)
|
260
|
+
root_node = xml.elements['TrackReply']
|
261
|
+
|
262
|
+
success = response_success?(xml)
|
263
|
+
message = response_message(xml)
|
264
|
+
|
265
|
+
if success
|
266
|
+
tracking_number, origin, destination = nil
|
267
|
+
shipment_events = []
|
268
|
+
|
269
|
+
tracking_details = root_node.elements['TrackDetails']
|
270
|
+
tracking_number = tracking_details.get_text('TrackingNumber').to_s
|
271
|
+
|
272
|
+
destination_node = tracking_details.elements['DestinationAddress']
|
273
|
+
destination = Address.new(
|
274
|
+
:country => destination_node.get_text('CountryCode').to_s,
|
275
|
+
:province => destination_node.get_text('StateOrProvinceCode').to_s,
|
276
|
+
:city => destination_node.get_text('City').to_s
|
277
|
+
)
|
278
|
+
|
279
|
+
tracking_details.elements.each('Events') do |event|
|
280
|
+
address = event.elements['Address']
|
281
|
+
|
282
|
+
city = address.get_text('City').to_s
|
283
|
+
state = address.get_text('StateOrProvinceCode').to_s
|
284
|
+
zip_code = address.get_text('PostalCode').to_s
|
285
|
+
country = address.get_text('CountryCode').to_s
|
286
|
+
next if country.blank?
|
287
|
+
|
288
|
+
location = Address.new(:city => city, :state => state, :postal_code => zip_code, :country => country)
|
289
|
+
description = event.get_text('EventDescription').to_s
|
290
|
+
|
291
|
+
# for now, just assume UTC, even though it probably isn't
|
292
|
+
time = Time.parse("#{event.get_text('Timestamp').to_s}")
|
293
|
+
zoneless_time = Time.utc(time.year, time.month, time.mday, time.hour, time.min, time.sec)
|
294
|
+
|
295
|
+
shipment_events << ShipmentEvent.new(description, zoneless_time, location)
|
296
|
+
end
|
297
|
+
shipment_events = shipment_events.sort_by(&:time)
|
298
|
+
end
|
299
|
+
|
300
|
+
TrackingResponse.new(success, message, Hash.from_xml(response),
|
301
|
+
:xml => response,
|
302
|
+
:request => last_request,
|
303
|
+
:shipment_events => shipment_events,
|
304
|
+
:destination => destination,
|
305
|
+
:tracking_number => tracking_number
|
306
|
+
)
|
307
|
+
end
|
308
|
+
|
309
|
+
def response_status_node(document)
|
310
|
+
document.elements['/*/Notifications/']
|
311
|
+
end
|
312
|
+
|
313
|
+
def response_success?(document)
|
314
|
+
%w{SUCCESS WARNING NOTE}.include? response_status_node(document).get_text('Severity').to_s
|
315
|
+
end
|
316
|
+
|
317
|
+
def response_message(document)
|
318
|
+
response_node = response_status_node(document)
|
319
|
+
"#{response_status_node(document).get_text('Severity').to_s} - #{response_node.get_text('Code').to_s}: #{response_node.get_text('Message').to_s}"
|
320
|
+
end
|
321
|
+
|
322
|
+
def commit(request, test = false)
|
323
|
+
ssl_post(test ? TEST_URL : LIVE_URL, request.gsub("\n",''))
|
324
|
+
end
|
325
|
+
|
326
|
+
def handle_uk_currency(currency)
|
327
|
+
currency =~ /UKL/i ? 'GBP' : currency
|
328
|
+
end
|
329
|
+
end
|
330
|
+
end
|