fedex 2.0.1 → 2.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +4 -1
- data/Readme.md +81 -3
- data/fedex.gemspec +2 -2
- data/lib/fedex/address.rb +30 -0
- data/lib/fedex/helpers.rb +5 -4
- data/lib/fedex/label.rb +44 -3
- data/lib/fedex/request/address.rb +92 -0
- data/lib/fedex/request/base.rb +39 -26
- data/lib/fedex/request/label.rb +10 -75
- data/lib/fedex/request/rate.rb +3 -3
- data/lib/fedex/request/shipment.rb +106 -0
- data/lib/fedex/request/tracking_information.rb +89 -0
- data/lib/fedex/shipment.rb +23 -1
- data/lib/fedex/tracking_information.rb +49 -0
- data/lib/fedex/tracking_information/event.rb +19 -0
- data/lib/fedex/version.rb +1 -1
- data/spec/lib/fedex/address_spec.rb +39 -0
- data/spec/lib/fedex/label_spec.rb +33 -18
- data/spec/lib/fedex/rate_spec.rb +149 -0
- data/spec/lib/fedex/shipment_spec.rb +33 -138
- data/spec/lib/fedex/track_spec.rb +50 -0
- data/spec/spec_helper.rb +4 -2
- data/spec/support/credentials.rb +15 -0
- metadata +29 -14
data/.gitignore
CHANGED
data/Readme.md
CHANGED
@@ -116,7 +116,7 @@ Fedex provides multiple total values; `total_net_charge` is the final amount you
|
|
116
116
|
@rate_zone="51">
|
117
117
|
```
|
118
118
|
|
119
|
-
### ** Generate a shipping label(PDF) **
|
119
|
+
### ** Generate a shipping label (PDF) **
|
120
120
|
|
121
121
|
To create a label for a shipment:
|
122
122
|
|
@@ -129,8 +129,84 @@ label = fedex.label(:filename => "my_dir/example.pdf",
|
|
129
129
|
:shipping_details => shipping_details)
|
130
130
|
```
|
131
131
|
|
132
|
-
|
133
|
-
|
132
|
+
### ** Generate a shipping label in any available format **
|
133
|
+
|
134
|
+
Change the filename extension and pass a label_specification hash. For example:
|
135
|
+
|
136
|
+
```ruby
|
137
|
+
example_spec = {
|
138
|
+
:image_type => "EPL2",
|
139
|
+
:label_stock_type => "STOCK_4X6"
|
140
|
+
}
|
141
|
+
|
142
|
+
label = fedex.label(:filename => "my_dir/example_epl2.pcx",
|
143
|
+
:shipper=>shipper,
|
144
|
+
:recipient => recipient,
|
145
|
+
:packages => packages,
|
146
|
+
:service_type => "FEDEX_GROUND",
|
147
|
+
:shipping_details => shipping_details,
|
148
|
+
:label_specification => example_spec)
|
149
|
+
```
|
150
|
+
|
151
|
+
### ** Tracking a shipment **
|
152
|
+
|
153
|
+
To track a shipment:
|
154
|
+
|
155
|
+
```ruby
|
156
|
+
tracking_info = fedex.track(:tracking_number => "1234567890123")
|
157
|
+
|
158
|
+
tracking_info.tracking_number
|
159
|
+
# => "1234567890123"
|
160
|
+
|
161
|
+
tracking_info.status
|
162
|
+
# => "Delivered"
|
163
|
+
|
164
|
+
tracking_info.events.first.description
|
165
|
+
# => "On FedEx vehicle for delivery"
|
166
|
+
```
|
167
|
+
|
168
|
+
### ** Tracking a shipment **
|
169
|
+
|
170
|
+
To track a shipment:
|
171
|
+
|
172
|
+
```ruby
|
173
|
+
tracking_info = fedex.track(:tracking_number => "1234567890123")
|
174
|
+
|
175
|
+
tracking_info.tracking_number
|
176
|
+
# => "1234567890123"
|
177
|
+
|
178
|
+
tracking_info.status
|
179
|
+
# => "Delivered"
|
180
|
+
|
181
|
+
tracking_info.events.first.description
|
182
|
+
# => "On FedEx vehicle for delivery"
|
183
|
+
```
|
184
|
+
|
185
|
+
### ** Verifying an address **
|
186
|
+
|
187
|
+
To verify an address is valid and deliverable:
|
188
|
+
|
189
|
+
```ruby
|
190
|
+
|
191
|
+
address = {
|
192
|
+
:street => "5 Elm Street",
|
193
|
+
:city => "Norwalk",
|
194
|
+
:state => "CT",
|
195
|
+
:postal_code => "06850",
|
196
|
+
:country => "USA"
|
197
|
+
}
|
198
|
+
|
199
|
+
address_result = fedex.validate_address(:address => address)
|
200
|
+
|
201
|
+
address_result.residential
|
202
|
+
# => true
|
203
|
+
|
204
|
+
address_result.score
|
205
|
+
# => 100
|
206
|
+
|
207
|
+
address_result.postal_code
|
208
|
+
# => "06850-3901"
|
209
|
+
```
|
134
210
|
|
135
211
|
# Services/Options Available
|
136
212
|
|
@@ -143,6 +219,8 @@ Fedex::Shipment::DROP_OFF_TYPES
|
|
143
219
|
# Contributors:
|
144
220
|
- [jazminschroeder](http://github.com/jazminschroeder) (Jazmin Schroeder)
|
145
221
|
- [parndt](https://github.com/parndt) (Philip Arndt)
|
222
|
+
- [mmell](https://github.com/mmell) (Michael Mell)
|
223
|
+
- [jordanbyron](https://github.com/jordanbyron) (Jordan Byron)
|
146
224
|
|
147
225
|
# Copyright/License:
|
148
226
|
Copyright 2011 [Jazmin Schroeder](http://jazminschroeder.com)
|
data/fedex.gemspec
CHANGED
@@ -9,8 +9,8 @@ Gem::Specification.new do |s|
|
|
9
9
|
s.authors = ["Jazmin Schroeder"]
|
10
10
|
s.email = ["jazminschroeder@gmail.com"]
|
11
11
|
s.homepage = "https://github.com/jazminschroeder/fedex"
|
12
|
-
s.summary = %q{Fedex
|
13
|
-
s.description = %q{
|
12
|
+
s.summary = %q{Fedex Web Services}
|
13
|
+
s.description = %q{Provides an interface to Fedex Web Services(version 10) - shipping rates, generate labels and address validation}
|
14
14
|
|
15
15
|
s.rubyforge_project = "fedex"
|
16
16
|
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Fedex
|
2
|
+
class Address
|
3
|
+
|
4
|
+
attr_reader :changes, :score, :confirmed, :available, :status, :residential,
|
5
|
+
:business, :street_lines, :city, :state, :province_code,
|
6
|
+
:postal_code, :country_code
|
7
|
+
|
8
|
+
def initialize(options)
|
9
|
+
@changes = options[:changes]
|
10
|
+
@score = options[:score].to_i
|
11
|
+
@confirmed = options[:delivery_point_validation] == "CONFIRMED"
|
12
|
+
@available = options[:delivery_point_validation] != "UNAVAILABLE"
|
13
|
+
|
14
|
+
@status = options[:residential_status]
|
15
|
+
@residential = status == "RESIDENTIAL"
|
16
|
+
@business = status == "BUSINESS"
|
17
|
+
|
18
|
+
address = options[:address]
|
19
|
+
|
20
|
+
@street_lines = address[:street_lines]
|
21
|
+
@city = address[:city]
|
22
|
+
@state = address[:state_or_province_code]
|
23
|
+
@province_code = address[:state_or_province_code]
|
24
|
+
@postal_code = address[:postal_code]
|
25
|
+
@country_code = address[:country_code]
|
26
|
+
|
27
|
+
@options = options
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
data/lib/fedex/helpers.rb
CHANGED
@@ -2,9 +2,10 @@ module Fedex
|
|
2
2
|
module Helpers
|
3
3
|
|
4
4
|
private
|
5
|
-
# String to CamelCase
|
6
|
-
def camelize(
|
7
|
-
|
5
|
+
# String or :symbol to CamelCase
|
6
|
+
def camelize(s)
|
7
|
+
# s.to_s.split('_').map { |e| e.capitalize }.join('')
|
8
|
+
s.to_s.gsub(/\/(.?)/) { "::#{$1.upcase}" }.gsub(/(?:^|_)(.)/) { $1.upcase }
|
8
9
|
end
|
9
10
|
|
10
11
|
# Helper method to validate required fields
|
@@ -16,4 +17,4 @@ module Fedex
|
|
16
17
|
key.to_s.sub(/^(v[0-9]+|ns):/, "").gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').gsub(/([a-z\d])([A-Z])/,'\1_\2').downcase
|
17
18
|
end
|
18
19
|
end
|
19
|
-
end
|
20
|
+
end
|
data/lib/fedex/label.rb
CHANGED
@@ -1,11 +1,52 @@
|
|
1
|
+
require 'base64'
|
2
|
+
require 'pathname'
|
3
|
+
|
1
4
|
module Fedex
|
2
5
|
class Label
|
3
|
-
attr_accessor :options
|
6
|
+
attr_accessor :options, :image, :response_details
|
4
7
|
|
5
8
|
# Initialize Fedex::Label Object
|
6
9
|
# @param [Hash] options
|
7
|
-
def initialize(
|
8
|
-
@
|
10
|
+
def initialize(label_details = {})
|
11
|
+
@response_details = label_details[:process_shipment_reply]
|
12
|
+
package_details = label_details[:process_shipment_reply][:completed_shipment_detail][:completed_package_details]
|
13
|
+
@options = package_details[:label]
|
14
|
+
@options[:format] = label_details[:format]
|
15
|
+
@options[:tracking_number] = package_details[:tracking_ids][:tracking_number]
|
16
|
+
@options[:file_name] = label_details[:file_name]
|
17
|
+
|
18
|
+
@image = Base64.decode64(options[:parts][:image]) if has_image?
|
19
|
+
|
20
|
+
if file_name = @options[:file_name]
|
21
|
+
save(file_name, false)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def name
|
26
|
+
[tracking_number, format].join('.')
|
27
|
+
end
|
28
|
+
|
29
|
+
def format
|
30
|
+
options[:format]
|
31
|
+
end
|
32
|
+
|
33
|
+
def tracking_number
|
34
|
+
options[:tracking_number]
|
35
|
+
end
|
36
|
+
|
37
|
+
def has_image?
|
38
|
+
options[:parts] && options[:parts][:image]
|
39
|
+
end
|
40
|
+
|
41
|
+
def save(path, append_name = true)
|
42
|
+
return unless has_image?
|
43
|
+
|
44
|
+
full_path = Pathname.new(path)
|
45
|
+
full_path = full_path.join(name) if append_name
|
46
|
+
|
47
|
+
File.open(full_path, 'wb') do|f|
|
48
|
+
f.write(@image)
|
49
|
+
end
|
9
50
|
end
|
10
51
|
end
|
11
52
|
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
require 'fedex/request/base'
|
2
|
+
require 'fedex/address'
|
3
|
+
require 'fileutils'
|
4
|
+
|
5
|
+
module Fedex
|
6
|
+
module Request
|
7
|
+
class Address < Base
|
8
|
+
def initialize(credentials, options={})
|
9
|
+
requires!(options, :address)
|
10
|
+
@credentials = credentials
|
11
|
+
@address = options[:address]
|
12
|
+
end
|
13
|
+
|
14
|
+
def process_request
|
15
|
+
api_response = self.class.post(api_url, :body => build_xml)
|
16
|
+
puts api_response if @debug == true
|
17
|
+
response = parse_response(api_response)
|
18
|
+
if success?(response)
|
19
|
+
options = response[:address_validation_reply][:address_results][:proposed_address_details]
|
20
|
+
|
21
|
+
Fedex::Address.new(options)
|
22
|
+
else
|
23
|
+
error_message = if response[:address_validation_reply]
|
24
|
+
[response[:address_validation_reply][:notifications]].flatten.first[:message]
|
25
|
+
else
|
26
|
+
api_response["Fault"]["detail"]["fault"]["reason"]
|
27
|
+
end rescue $1
|
28
|
+
raise RateError, error_message
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
# Build xml Fedex Web Service request
|
35
|
+
def build_xml
|
36
|
+
builder = Nokogiri::XML::Builder.new do |xml|
|
37
|
+
xml.AddressValidationRequest(:xmlns => "http://fedex.com/ws/addressvalidation/v2"){
|
38
|
+
add_web_authentication_detail(xml)
|
39
|
+
add_client_detail(xml)
|
40
|
+
add_version(xml)
|
41
|
+
add_request_timestamp(xml)
|
42
|
+
add_address_validation_options(xml)
|
43
|
+
add_address_to_validate(xml)
|
44
|
+
}
|
45
|
+
end
|
46
|
+
builder.doc.root.to_xml
|
47
|
+
end
|
48
|
+
|
49
|
+
def add_request_timestamp(xml)
|
50
|
+
timestamp = Time.now
|
51
|
+
|
52
|
+
# Calculate current timezone offset manually.
|
53
|
+
# Ruby <= 1.9.2 does not support this in Time#strftime
|
54
|
+
#
|
55
|
+
utc_offest = "#{timestamp.gmt_offset < 0 ? "-" : "+"}%02d:%02d" %
|
56
|
+
(timestamp.gmt_offset / 60).abs.divmod(60)
|
57
|
+
timestamp = timestamp.strftime("%Y-%m-%dT%H:%M:%S") + utc_offest
|
58
|
+
|
59
|
+
xml.RequestTimestamp timestamp
|
60
|
+
end
|
61
|
+
|
62
|
+
def add_address_validation_options(xml)
|
63
|
+
xml.Options{
|
64
|
+
xml.CheckResidentialStatus true
|
65
|
+
}
|
66
|
+
end
|
67
|
+
|
68
|
+
def add_address_to_validate(xml)
|
69
|
+
xml.AddressesToValidate{
|
70
|
+
xml.Address{
|
71
|
+
xml.StreetLines @address[:street]
|
72
|
+
xml.City @address[:city]
|
73
|
+
xml.StateOrProvinceCode @address[:state]
|
74
|
+
xml.PostalCode @address[:postal_code]
|
75
|
+
xml.CountryCode @address[:country]
|
76
|
+
}
|
77
|
+
}
|
78
|
+
end
|
79
|
+
|
80
|
+
def service
|
81
|
+
{ :id => 'aval', :version => 2 }
|
82
|
+
end
|
83
|
+
|
84
|
+
# Successful request
|
85
|
+
def success?(response)
|
86
|
+
response[:address_validation_reply] &&
|
87
|
+
%w{SUCCESS WARNING NOTE}.include?(response[:address_validation_reply][:highest_severity])
|
88
|
+
end
|
89
|
+
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
data/lib/fedex/request/base.rb
CHANGED
@@ -17,9 +17,6 @@ module Fedex
|
|
17
17
|
# Fedex Production URL
|
18
18
|
PRODUCTION_URL = "https://gateway.fedex.com:443/xml/"
|
19
19
|
|
20
|
-
# Fedex Version number for the Fedex service used
|
21
|
-
VERSION = 10
|
22
|
-
|
23
20
|
# List of available Service Types
|
24
21
|
SERVICE_TYPES = %w(EUROPE_FIRST_INTERNATIONAL_PRIORITY FEDEX_1_DAY_FREIGHT FEDEX_2_DAY FEDEX_2_DAY_AM FEDEX_2_DAY_FREIGHT FEDEX_3_DAY_FREIGHT FEDEX_EXPRESS_SAVER FEDEX_FIRST_FREIGHT FEDEX_FREIGHT_ECONOMY FEDEX_FREIGHT_PRIORITY FEDEX_GROUND FIRST_OVERNIGHT GROUND_HOME_DELIVERY INTERNATIONAL_ECONOMY INTERNATIONAL_ECONOMY_FREIGHT INTERNATIONAL_FIRST INTERNATIONAL_PRIORITY INTERNATIONAL_PRIORITY_FREIGHT PRIORITY_OVERNIGHT SMART_POST STANDARD_OVERNIGHT)
|
25
22
|
|
@@ -48,6 +45,7 @@ module Fedex
|
|
48
45
|
requires!(options, :shipper, :recipient, :packages, :service_type)
|
49
46
|
@credentials = credentials
|
50
47
|
@shipper, @recipient, @packages, @service_type, @customs_clearance, @debug = options[:shipper], options[:recipient], options[:packages], options[:service_type], options[:customs_clearance], options[:debug]
|
48
|
+
@debug = ENV['DEBUG'] == 'true'
|
51
49
|
@shipping_options = options[:shipping_options] ||={}
|
52
50
|
end
|
53
51
|
|
@@ -73,14 +71,18 @@ module Fedex
|
|
73
71
|
xml.ClientDetail{
|
74
72
|
xml.AccountNumber @credentials.account_number
|
75
73
|
xml.MeterNumber @credentials.meter
|
74
|
+
xml.Localization{
|
75
|
+
xml.LanguageCode 'en' # English
|
76
|
+
xml.LocaleCode 'us' # United States
|
77
|
+
}
|
76
78
|
}
|
77
79
|
end
|
78
80
|
|
79
81
|
# Add Version to xml request, using the latest version V10 Sept/2011
|
80
82
|
def add_version(xml)
|
81
83
|
xml.Version{
|
82
|
-
xml.ServiceId
|
83
|
-
xml.Major
|
84
|
+
xml.ServiceId service[:id]
|
85
|
+
xml.Major service[:version]
|
84
86
|
xml.Intermediate 0
|
85
87
|
xml.Minor 0
|
86
88
|
}
|
@@ -164,12 +166,22 @@ module Fedex
|
|
164
166
|
xml.Units package[:weight][:units]
|
165
167
|
xml.Value package[:weight][:value]
|
166
168
|
}
|
167
|
-
|
168
|
-
xml.
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
169
|
+
if package[:dimensions]
|
170
|
+
xml.Dimensions{
|
171
|
+
xml.Length package[:dimensions][:length]
|
172
|
+
xml.Width package[:dimensions][:width]
|
173
|
+
xml.Height package[:dimensions][:height]
|
174
|
+
xml.Units package[:dimensions][:units]
|
175
|
+
}
|
176
|
+
end
|
177
|
+
if package[:customer_refrences]
|
178
|
+
xml.CustomerReferences{
|
179
|
+
package[:customer_refrences].each do |value|
|
180
|
+
xml.CustomerReferenceType 'CUSTOMER_REFERENCE'
|
181
|
+
xml.Value value
|
182
|
+
end
|
183
|
+
}
|
184
|
+
end
|
173
185
|
}
|
174
186
|
end
|
175
187
|
end
|
@@ -177,7 +189,7 @@ module Fedex
|
|
177
189
|
# Add customs clearance(for international shipments)
|
178
190
|
def add_customs_clearance(xml)
|
179
191
|
xml.CustomsClearanceDetail{
|
180
|
-
|
192
|
+
hash_to_xml(xml, @customs_clearance)
|
181
193
|
}
|
182
194
|
end
|
183
195
|
|
@@ -192,22 +204,22 @@ module Fedex
|
|
192
204
|
raise NotImplementedError, "Override build_xml in subclass"
|
193
205
|
end
|
194
206
|
|
195
|
-
# Build nodes
|
196
|
-
def
|
207
|
+
# Build xml nodes dynamically from the hash keys and values
|
208
|
+
def hash_to_xml(xml, hash)
|
197
209
|
hash.each do |key, value|
|
210
|
+
element = camelize(key)
|
198
211
|
if value.is_a?(Hash)
|
199
|
-
xml.send
|
200
|
-
|
212
|
+
xml.send element do |x|
|
213
|
+
hash_to_xml(x, value)
|
201
214
|
end
|
202
215
|
elsif value.is_a?(Array)
|
203
|
-
node = key
|
204
216
|
value.each do |v|
|
205
|
-
xml.send
|
206
|
-
|
207
|
-
end
|
208
|
-
end
|
217
|
+
xml.send element do |x|
|
218
|
+
hash_to_xml(x, v)
|
219
|
+
end
|
220
|
+
end
|
209
221
|
else
|
210
|
-
xml.send
|
222
|
+
xml.send element, value
|
211
223
|
end
|
212
224
|
end
|
213
225
|
end
|
@@ -217,7 +229,7 @@ module Fedex
|
|
217
229
|
response = sanitize_response_keys(response)
|
218
230
|
end
|
219
231
|
|
220
|
-
# Recursively sanitizes the response object by
|
232
|
+
# Recursively sanitizes the response object by cleaning up any hash keys.
|
221
233
|
def sanitize_response_keys(response)
|
222
234
|
if response.is_a?(Hash)
|
223
235
|
response.inject({}) { |result, (key, value)| result[underscorize(key).to_sym] = sanitize_response_keys(value); result }
|
@@ -228,8 +240,9 @@ module Fedex
|
|
228
240
|
end
|
229
241
|
end
|
230
242
|
|
231
|
-
def
|
232
|
-
|
243
|
+
def service
|
244
|
+
raise NotImplementedError,
|
245
|
+
"Override service in subclass: {:id => 'service', :version => 1}"
|
233
246
|
end
|
234
247
|
|
235
248
|
# Use GROUND_HOME_DELIVERY for shipments going to a residential address within the US.
|
@@ -248,4 +261,4 @@ module Fedex
|
|
248
261
|
|
249
262
|
end
|
250
263
|
end
|
251
|
-
end
|
264
|
+
end
|