fedex 2.0.1 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|